357 Commits

Author SHA1 Message Date
bd1c6147fd 2.3.0 2025-05-09 19:38:39 +02:00
891abda0fb refactor: remove wip esptool section 2025-05-07 21:26:19 +02:00
3611f65e24 fix: build failure 2025-05-06 16:36:50 +02:00
f76882a09c feat: new settings page design 2025-05-06 16:34:34 +02:00
ff7e4f7b2e feat: add version changelog support 2025-05-06 15:04:44 +02:00
1c1c86241f fix: M4G isn't listed in the device manager 2025-05-02 17:40:04 +02:00
dc8b3c3d66 fix: update product ids 2025-05-02 13:21:35 +02:00
Aleksandr Iushmanov
65911419b0 Remove unused direction with incomplete type definition. (#184) 2025-04-27 23:14:21 +02:00
Aleksandr Iushmanov
ccfb09e261 exclude openssl and i18n from npm run format; + npm run format (#183) 2025-04-27 15:43:16 +02:00
b841469505 feat: add icons 2025-04-25 21:42:07 +02:00
bc06e8ee80 feat: color picker for hsv settings 2025-04-23 15:56:58 +02:00
24fc861ef4 fix: commit is not being sent when only settings or layout change 2025-04-22 19:56:24 +02:00
5801e5fbbe feat: ota progress bar
fix: can't set settings with inverse/scale
2025-04-22 19:14:51 +02:00
92b52e08f7 fix: progress bar is broken
fixes #175
2025-04-22 15:19:00 +02:00
4192210d27 fix: use different icons for consumer control
fixes #174
2025-04-22 14:30:21 +02:00
Aleksandr Iushmanov
0e5640a1ee [#167] Expand textarea for sentence input; use untrack to break recursive reactivity loops hanging the page on long sentences; Use better error message instead of ERROR (#182) 2025-04-22 14:25:44 +02:00
7f27499003 ci: update gh actions 2025-04-17 13:55:27 +02:00
Aleksandr Iushmanov
b6ded5f94c Remove unused CSS selectors. (#181) 2025-04-17 13:13:53 +02:00
Aleksandr Iushmanov
63d0ad7ae8 Use modern compiler for css processing in vite (to remove SASS 2.0.0 warnings on deprecated JS API usage); (#180)
Resolve some of SASS deprecation warnings;
Add note to readme about icons generation
2025-04-08 11:51:36 +02:00
Aleksandr Iushmanov
1c8f53caf6 Allow adding arrows as chord actions when shift is pressed (#179) 2025-04-06 17:41:14 +02:00
1d60b12d43 fix: settings not showing for older devices 2025-04-04 18:15:07 +02:00
e85a731410 feat: fetch settings from build meta 2025-04-04 18:03:09 +02:00
050af564ab ci: add pull request workflows 2025-03-31 14:00:32 +02:00
6545124aa2 refactor: update dependencies 2025-03-31 13:50:59 +02:00
b93724add3 Merge pull request #178 from poweroftrue/master
Improve chord loading speed
2025-03-31 13:13:17 +02:00
Mostafa Dahab
e1092113f6 Improve chord loading speed
Signed-off-by: Mostafa Dahab <mostafa@dahab.io>
2025-03-30 16:29:58 +03:00
Izeren
0bb4bbe838 [#169] Fix autoConnect feature. Use value form persistent storage when it is available. (#170)
Authored-by: izeren <yushalnik@bk.ru>
2025-02-24 11:30:19 +01:00
089812c555 feat: better connection error messages
resolve #159
fixes #158
fixes #153
2025-02-14 16:31:43 +01:00
45c5f21cc4 fix: search shows 1 chord when there are 0
fixes #151
2025-02-14 15:42:19 +01:00
fb5959998a feat: sentence trainer custom prompt
resolves #162
2025-02-14 15:31:17 +01:00
f319714489 fix: duplicate chords crash
fix: duplicate confirm dialog does not show affected chord
fixes #137
fixes #163
2025-02-14 15:17:22 +01:00
fb1f5b7ec7 fix: can't type in chords 2025-02-14 14:55:06 +01:00
ac16cfd3bf feat: use factory default meta
feat: clear chords button
resolves #64
2025-02-14 14:52:07 +01:00
9d5b0e01d2 feat: add voicebox shortcut
resolves #160
2025-02-14 14:04:45 +01:00
e7517f821d fix: action search does not work until selecting a filter
fixes #149
2025-02-14 13:38:47 +01:00
762f73063a fix: vocabulary is eating spaces
fixes #115
2025-02-14 13:33:44 +01:00
7ca9e04dd3 feat: use version meta
fixes #150
2025-02-13 16:17:46 +01:00
4d73dad780 feat: learn chat message 2025-02-13 13:53:01 +01:00
5419824c06 feat: re-add chat
fixes #161
2025-02-13 13:33:12 +01:00
075d05dd0b feat: better update page
feat: hide manual update steps as "unsafe" if OTA is available

resolves #155
2025-02-12 16:00:50 +01:00
9266702cbb feat: add sentence wpm stage 2025-01-16 20:41:00 +01:00
77e2d2b20e feat: sentence trainer idle timeout 2025-01-16 17:50:52 +01:00
7819f546a6 fix: package manager 2025-01-16 17:15:25 +01:00
e37b38085d feat: sentence trainer prototype
feat: layout learner prototype
2025-01-16 17:12:56 +01:00
a3bf9ac32b 2.2.3 2025-01-15 11:31:47 +01:00
David Villafaña
5bd3245084 fix: typographical error (#156)
Co-authored-by: David Rog Desktop <dvillafanaiv@proton.me>
2025-01-15 11:30:46 +01:00
1cd2ec318a 2.2.2 2025-01-14 13:35:53 +01:00
6c8bfa0272 fix: ota update 2025-01-14 13:31:22 +01:00
f69be14b5e 2.2.1 2025-01-06 19:34:28 +01:00
dce554fc66 fix: set pnpm version in github actions correctly 2025-01-06 19:32:43 +01:00
f152dbdcf5 fix: set node/pnpm versions correctly 2025-01-06 19:31:04 +01:00
6a29e6a2fc 2.2.0 2025-01-06 19:25:45 +01:00
9bf3801fef Mark factory flash as wip 2025-01-06 19:25:27 +01:00
d2accfb838 Squash merge fix-vocabulary-export into master 2024-12-09 18:41:26 +01:00
b8a376b93b feat: update m4g 2024-12-09 18:35:05 +01:00
588719df91 feat: support factory flashing 2024-11-23 19:02:35 +01:00
6a0dad9dad feat: android support 2024-11-23 15:07:35 +01:00
f3704e4051 2.1.0 2024-11-20 22:26:59 +01:00
3e6298717e feat: m4gr 2024-11-19 22:25:01 +01:00
aced0bbbb7 feat: m4g support 2024-11-19 17:48:50 +01:00
Raymond Li
36874c59e3 Temporarily make chat available 2024-11-19 06:08:37 +00:00
9dc61a3482 fix: exclude pre-rendered ccos update pages 2024-11-08 16:04:50 +01:00
d9183f952a 2.0.2 2024-11-08 15:48:26 +01:00
913a833824 fix: build 2024-11-08 15:47:20 +01:00
0d6ef4d011 2.0.1 2024-11-08 15:43:23 +01:00
232045964c fix: firmware updates 2024-11-08 15:42:58 +01:00
3659b80e41 fix: firmware cannot be linked 2024-11-08 15:21:53 +01:00
3a02caeb6d fix: pre-production devices are not recognized by the device manager 2024-11-07 21:53:59 +01:00
259fd3a989 fix: stable pipeline 2024-11-05 02:51:21 +01:00
dcf1d89fa0 2.0.0 2024-11-05 02:39:46 +01:00
c79237ce22 move matrix init 2024-11-05 02:38:34 +01:00
d68f1b19fa update dependencies 2024-11-05 02:34:40 +01:00
9cb36662b3 polish 2024-11-05 02:03:08 +01:00
b4605fe84d feat: improve UF2 flow 2024-11-03 14:39:35 +01:00
06d122b5d6 feat: add changed pids 2024-10-18 12:18:49 +02:00
3d25b030c6 feat: explicit reboot after ota update 2024-10-16 19:34:16 +02:00
bf490ba823 fix: swapped pid/vid 2024-10-04 18:08:26 +02:00
397f4bb6a9 update compatibility list 2024-10-03 21:32:58 +02:00
1f4604bcbc fix: correctly show compatibility 2024-09-29 22:34:12 +02:00
68faf57a22 ota update flow 2024-09-29 22:25:03 +02:00
1d976947e1 fix: server load interferes with spa 2024-09-29 20:27:06 +02:00
ca8bfac3bc update deployment 2024-09-29 19:33:20 +02:00
2f0d8f2e1d feat: matrix 2024-09-29 02:00:52 +02:00
236e23086c ota suppor 2024-09-29 02:00:29 +02:00
d1fefb88a1 feat: matrix 2024-09-13 21:35:52 +02:00
26c43b1966 feat: learn 2024-08-21 18:20:04 +02:00
8b2bfee099 feat: multi-purpose site
feat: editor
feat: plugin editor
2024-08-01 01:31:04 +02:00
b8b903c5e1 refactor: update to Svelte 5 preview
feat: add charrecorder
feat: dynamic os layouts for CC1
2024-08-01 00:28:38 +02:00
6201cf5b0c feat: update dynamic library description 2024-07-24 19:19:20 +02:00
aaafadf732 fix: pid/vid wrong 2024-07-24 19:07:18 +02:00
fe80867ce4 feat: M4G support 2024-07-24 18:28:47 +02:00
72a8e084ce fix: plugins can't execute plugins 2024-07-16 15:21:34 +02:00
989e844190 fix: compound order 2024-07-11 13:40:31 +02:00
500221f39a feat: experimental support for compounds 2024-07-11 13:38:19 +02:00
Raymond Li
d91273d27b Update CONTRIBUTING.md 2024-07-10 00:22:40 +02:00
888df6dd66 1.5.2 2024-07-09 16:43:06 +02:00
7ad9612037 fix: add pnpm to pipeline 2024-07-09 16:39:21 +02:00
3f9674b399 fix: pwa prevents layout share url from being loaded 2024-07-09 16:29:28 +02:00
92ba5bcb24 fix: build 2024-07-09 16:28:42 +02:00
2163a63a7c fix: release build pipeline 2024-07-08 18:51:09 +02:00
65a5a2517e feat: improvements 2024-07-08 18:43:06 +02:00
21e8c291b0 fix: compatibility issues 2024-07-08 09:26:51 +02:00
4106a80d53 feat: improve device support 2024-06-08 17:34:18 +02:00
John de St Germain
01fb61d27c Fix misspelling 2024-05-13 21:39:14 +02:00
3dd91a1cea 1.5.1 2024-04-29 11:19:37 +02:00
cbcf705f71 feat: massively improved chord search
fixes #119
2024-04-29 11:18:23 +02:00
4007810c7b fix: can't edit blank actions
fixes #110
2024-04-29 09:35:22 +02:00
f322435c41 1.5.0 2024-04-26 17:13:55 +02:00
587375e654 fix: chord conflict shows "undefined" 2024-04-26 17:12:59 +02:00
0500a723de fix: remove tab hotkeys 2024-04-26 17:04:39 +02:00
26dcc56aca feat: and the ability to duplicate chords
resolves #100
2024-04-23 18:21:04 +02:00
20b65813bf fix: chord action change indicator has weird placement 2024-04-23 18:06:04 +02:00
87b23c04b1 fix: strikethrough misaligned 2024-04-23 17:56:53 +02:00
8b2bc6d109 fix: chord page auto-focuses input on first load
fixes #111
2024-04-23 17:46:05 +02:00
19cf0b26b3 feat: add vocabulary export 2024-04-23 17:38:33 +02:00
3e72dd3cb8 fix: new actions show as compound chords
fixes #107
feat: add ability to edit compound chords
2024-04-23 17:16:21 +02:00
a40daefbad fix: action selector auto-focus
fixes #108
2024-04-23 16:57:53 +02:00
77d4a90519 fix: can't cancel the chord input recording
fixes #109
2024-04-23 16:52:36 +02:00
c9a031a1fd fix: hitting the enter key when focusing elements opens the reset menu
fixes #114
2024-04-23 16:48:18 +02:00
254a0c1aec fix: chording press/release has wrong max values
fixes #113
2024-04-23 16:33:58 +02:00
bd75012cf1 fix: svelte-check issues 2024-04-06 19:25:03 +02:00
4b738bb340 fix: hotkeys
fixes #20
2024-04-06 19:05:42 +02:00
3af65106bf feat: auto-focus new chords 2024-04-06 18:08:16 +02:00
8087d10d5a fix: auto focus reset challenge input 2024-04-06 18:06:10 +02:00
2782966505 fix: action search
fix: can't browse actions after searching and clearing
fix: can't use esc to exit action search
fix: improve action search performance
2024-04-06 18:04:13 +02:00
5b6d369101 feat: add pre-ccos hint when connection errors
resolves #99
2024-04-06 17:43:43 +02:00
b423d1c661 feat: add store link
feat: rebalance footer
resolves #88
2024-04-06 17:38:27 +02:00
92a3c6012f feat: add random tips to the chords page
resolves #81
2024-04-06 17:33:46 +02:00
8ec11c7ec9 fix: reset options challenge box not filling the dialog
fixes #87
2024-04-06 16:54:56 +02:00
5c8eb1d19f feat: allow creation of single letter chords
resolves #84
2024-04-06 16:52:18 +02:00
91a044bbba fix: some ccx stuff 2024-04-06 16:49:30 +02:00
1a6c85a361 fix: can't search ccx chords
fixes #98

feat: improve search page responsiveness
2024-04-06 16:42:10 +02:00
ecef11ac2d fix: settings page header change indicator 2024-04-06 16:12:20 +02:00
a23af9ba9d fix: lite rgb 2024-04-06 15:56:02 +02:00
93849f250f feat: fully expand linux permission guide
fixes #103
2024-04-06 15:46:31 +02:00
33890b0aa8 feat: improve responsiveness 2024-04-06 15:37:13 +02:00
6f925de1af feat: charachorder lite brightness & color settings 2024-04-06 14:41:26 +02:00
d45fe43f17 feat: and warning about flatpak and snaps
resolves #104
2024-04-06 14:34:20 +02:00
59788f059d fix: add ascii plus
fixes #105
2024-04-06 14:30:24 +02:00
2808973ad0 feat: enable stricter type checking options
feat: make the app more fault tolerant
2024-04-06 14:28:23 +02:00
bef51d2a7d refactor: update dependencies 2024-04-06 13:32:53 +02:00
854ab6d3be refactor: use standard prettier formatting 2024-04-06 13:15:35 +02:00
86ec8651b6 feat: some forced color adjustments 2024-03-16 14:41:39 +01:00
4e4bff02a0 feat: react to user contrast preferences 2024-03-16 13:09:21 +01:00
5d4dbc7e2a feat: improve legebility for inactive layout layers 2024-03-15 23:46:37 +01:00
dfd1c0bcbd feat: add suspense logs in serial console 2024-03-05 18:14:49 +01:00
6ac2cd1993 fix: add timeout for device responses 2024-03-05 18:12:56 +01:00
7256dc50d4 feat: new action codes 2024-03-04 21:07:45 +01:00
f0ad19e6c2 1.4.0 2024-02-14 00:55:10 +01:00
9022a09b4c fix: allow 0-return on chord deletion 2024-02-07 00:11:10 +01:00
7e3e61afd7 feat: force backup when putting the device into bootloader mode 2024-02-05 21:03:36 +01:00
08f594d164 feat: read full chord actions every time
feat: add special view for compound chords
fix: make it possible to delete compound chords
fixes #94
2024-02-05 20:39:05 +01:00
046595b51f feat: add device firmware update instructions
resolves #89
2024-02-05 20:08:50 +01:00
fbc5303690 fix: backup title is confusing
fixes #83
2024-02-05 19:55:26 +01:00
ad41d39bfb fix: remove logging statement
fixes #80
2024-02-05 19:50:37 +01:00
6faaa18b3b 1.3.2 2024-01-30 19:49:52 +01:00
6ab6959129 fix: disallow null inputs when editing
feat: allow special inputs while creating a chord input
fixes #93
2024-01-30 19:49:10 +01:00
44d89d3f35 1.3.1 2024-01-24 18:55:46 +01:00
eaf0adaf01 fix: sort legacy chord inputs 2024-01-24 18:55:31 +01:00
5b6a5ea36d 1.3.0 2024-01-20 22:24:39 +01:00
14cbb5553b feat: add auto-space info 2024-01-20 22:24:00 +01:00
duianto
8ed72fe958 fix: typo 2024-01-11 09:36:33 +01:00
06b83f79ef feat: add refresh button
resolves #82
2024-01-05 00:12:42 +01:00
5fa4b1fd09 1.2.0 2024-01-03 14:59:12 +01:00
f585a0ebda fix: disable Tauri publish for now 2024-01-03 14:50:51 +01:00
a48e2b5a16 fix: keyinfo missing display type prop 2024-01-03 14:21:37 +01:00
fd612eda1d fix: dynamic mappings are not displayed 2024-01-03 14:21:13 +01:00
a1fe6f7110 feat: periodically update os-layout in the background
fix: remove dead code in layout detection
fixes #78
resolves #79
2024-01-03 13:55:50 +01:00
0e57e810e0 feat: change icons 2024-01-03 01:26:39 +01:00
a15d5dde38 feat: inform user when save action failed
fixes #67
2023-12-30 16:04:16 +01:00
560206129e feat: add meta for config pages 2023-12-30 16:03:31 +01:00
cb7c70dac1 refactor: flatten visual key positioning system
fixes #74
fixes #43
2023-12-30 15:50:48 +01:00
edabf8ec84 fix: settings without min/max parse as 0
fixes #75
2023-12-30 12:50:46 +01:00
f2f61f32f2 feat: add reset options
resolves #70
2023-12-29 15:04:33 +01:00
a3857843d6 feat: use keycodes on CCX
resolves #71
2023-12-29 13:48:34 +01:00
c1b1068c4b fix: settings.yml missing hex prefix
feat: add direnv config
2023-12-29 13:23:41 +01:00
2411dd2bea feat: show dynamic key maps in layout view 2023-12-22 12:51:23 +01:00
7911904906 Revert "refactor: remove outdated files"
This reverts commit 84b22e0006.
2023-12-21 23:55:29 +01:00
630687de80 fix: use hexadecimal for settings 2023-12-21 23:49:55 +01:00
84b22e0006 refactor: remove outdated files 2023-12-21 23:39:23 +01:00
dd070c8856 feat: include source maps in pwa 2023-12-21 21:31:43 +01:00
6872cd0554 fix: build 2023-12-21 21:24:12 +01:00
628007af23 fix: align settings wording with gtm 2023-12-21 21:23:00 +01:00
19fad84357 fix: svg invalid 2023-12-21 21:20:26 +01:00
f172318a78 feat: update logo and favicon 2023-12-21 21:19:17 +01:00
c2e3850082 fix: maybe fix cloudflare pages loop 2023-12-21 20:47:06 +01:00
7a5a4eb434 feat: update pwa icon 2023-12-21 20:46:12 +01:00
c878311f62 feat: add web manifest to site meta 2023-12-21 20:28:47 +01:00
fb3fb246e9 fix: remove json files from pwa glob pattern 2023-12-21 20:19:55 +01:00
b4e4ca84a4 fix: pwa tries to include build-only files 2023-12-21 20:05:32 +01:00
c1b1544256 fix: mouse & scroll-speed options are unstyled
resolves #44
2023-12-21 19:42:37 +01:00
03dd528465 feat: add ability to add special actions to chord inputs
resolves #10
2023-12-21 19:19:47 +01:00
81af9f2e82 feat: add min/max enforcement to device settings
resolves #6
2023-12-21 18:31:50 +01:00
6bb42429e5 feat: add change indicator on settings page 2023-12-21 18:23:47 +01:00
d07751a944 fix: remove OS setting
resolves #61
2023-12-21 18:12:37 +01:00
8867030ede fix: PWA fixes 2023-12-21 18:08:16 +01:00
faaa6dd5be fix: keyboard action variant wrong 2023-12-21 16:11:02 +01:00
43cf13094e fix: "p" key missing for ccx and linked with "o" key 2023-12-20 20:01:28 +01:00
ed523628ff feat: try typing field in chords section
resolves #68
fix: "No Results" not translated
2023-12-18 18:42:08 +01:00
98b451eec9 1.1.0 2023-12-17 00:33:11 +01:00
6e37dc198f feat: rework character timeout setting 2023-12-16 18:13:02 +01:00
e319b1bfaf fix: swap top/bottom thumb labels
fixes #65
2023-12-16 15:27:34 +01:00
eb33b64100 feat: reject new chords that override another chord 2023-12-16 15:24:51 +01:00
766bc44a85 feat: do not use empty phrase for deleted chords 2023-12-16 15:20:44 +01:00
b679aa377a fix: key text showing focus outline
fix: layout selectable
2023-12-16 13:11:52 +01:00
ea3192d4e6 feat: add links to docs and dotio 2023-12-16 12:46:20 +01:00
256daec412 feat: chord modifier hints 2023-12-15 19:54:31 +01:00
29a07133d1 fix: deadlock 2023-12-15 16:59:06 +01:00
c3bd8431e5 feat: debounce connection suspension 2023-12-15 16:43:56 +01:00
c8e04ed6cc feat: auto-reconnect after reboot 2023-12-12 18:37:40 +01:00
d98653995b feat: bootloader warning
refactor: reword linux premission warning
2023-12-12 18:30:20 +01:00
3dd9611ebf feat: Linux permission guide 2023-12-12 18:03:34 +01:00
9d9360375b 1.0.0 2023-12-08 23:52:33 +01:00
d683c8c70c fix: action selector shows next item every time 2023-12-08 23:21:21 +01:00
d8d430f333 fix: browser warning referencing a non-existent app 2023-12-08 23:12:52 +01:00
fe850f47ec feat: add info for current and after next save action 2023-12-08 23:04:38 +01:00
f9a63a8724 fix: warn users if no device is connected
fix: can't backup without a device
2023-12-08 22:55:33 +01:00
af01426f43 fix: action tooltips not updating 2023-12-08 22:46:01 +01:00
9d7cefb3b4 fix: add ui when no device is connected
fixes #60
2023-12-08 22:38:46 +01:00
f44e5a79de fix: action selector search bar color
fixes #39
2023-12-08 22:30:03 +01:00
8b2e92c124 feat: add icons to unassigned cc1 3D keys
fixes #58
2023-12-08 22:26:32 +01:00
f758be91a9 fix: build 2023-12-08 22:16:07 +01:00
bf4c86e698 fix: legacy chords with commas and spaces 2023-12-08 22:10:49 +01:00
50a09d2008 fix: PWA not working 2023-12-08 22:02:48 +01:00
3c1a4de4a7 fix: chord page overlapping
fixes #57, fixes #56
fix: handle trailing spaces in lecacy chord files
2023-12-08 21:59:08 +01:00
8cbdf1393f fix: chord files not detected properly
feat: alert on unknown backups
2023-12-08 21:14:37 +01:00
1ccb17f053 fix: allow trailing linebreak for legacy layouts 2023-12-08 21:04:18 +01:00
532dc70fe2 feat: ccx layout 2023-12-08 14:49:07 +01:00
d5893013f9 feat: ccx layout maybe 2023-12-08 14:13:31 +01:00
80308cad73 feat: ccx layout (hopefully) 2023-12-07 21:52:04 +01:00
2d59bd016f feat: ccx row 2 2023-12-07 21:40:27 +01:00
298de49257 test ccx row 2023-12-07 21:36:31 +01:00
3a62864a41 feat: ccx key count 2023-12-07 21:29:26 +01:00
109095e35e feat: re-introduce background sync 2023-12-07 19:51:23 +01:00
2dd6f39ac6 fix: reword alt-code warnings
fixes #11
2023-12-07 19:36:10 +01:00
b0f653e73b fix: weird input behaviour on setting changes 2023-12-07 19:27:29 +01:00
d552fb9220 fix: only import settings that already exist 2023-12-07 19:06:49 +01:00
77339620e6 fix: full backups fail because of invalid setting IDs 2023-12-07 18:58:40 +01:00
846183bbb1 feat: compound chording actions 2023-12-06 01:19:01 +01:00
1d53f6df7a fix: crash with missing action info in chords 2023-12-06 00:31:08 +01:00
58d13a4107 feat: enable source maps in production builds 2023-12-05 17:29:15 +01:00
f7d99d8d7b feat: dynamic keymap prototype 2023-12-03 00:01:51 +01:00
d9dd003b01 feat: show warnings about shift and alt-code macros
resolves #38
2023-12-02 23:26:04 +01:00
dc798d2b9f v0.7.0 2023-12-02 21:23:34 +01:00
c2ec460c8c feat: alert user when connection failed, resolves #53 2023-12-02 21:18:56 +01:00
c51bcc8ff0 feat: legacy backup import, resolves #31 2023-12-02 21:16:22 +01:00
63b7f8ab18 feat: inform user when backup is incompatible, resolves #34 2023-12-02 21:03:14 +01:00
eaf8028538 feat: chord sharing url, resolves #40 2023-12-02 20:59:58 +01:00
2ad0ef3b6d feat: show info about key in chord manager and layout, resolves #51, resolves #52 2023-12-02 20:42:33 +01:00
20705de069 fix: editing chords bouces back to page 1, fixes #22 2023-12-02 20:31:39 +01:00
64b519d5b1 fix: can't type in the terminal 2023-12-02 20:25:10 +01:00
fb490b3db6 fix: page transitions can be buggy, fixes #55, #23 2023-12-02 20:24:42 +01:00
c37ae7da7b refactor: adjust wording for backups 2023-12-02 20:23:15 +01:00
5c06c2206c fix: undo/redo prevents use of unknown actions 2023-12-02 19:31:46 +01:00
f9cdf70bdb change save button styling
resolves #49
2023-11-29 14:31:19 +01:00
3a6483aa61 feat: combine save/apply
resolves #45
2023-11-29 01:08:46 +01:00
Priyanshu Tripathi
018c7a5eac fix: link to the new repository
The old repo will soon fall behind in terms of releases and the link will become outdated.
2023-11-28 17:35:43 +01:00
f73b8c1453 fix: set auto-connect to false by default
fixes #25
2023-11-26 22:17:52 +01:00
e38d952e1d fix: imported chords not filtered 2023-11-18 18:59:30 +01:00
8e5692ca59 fix: imported chords not filtered 2023-11-18 18:49:15 +01:00
a0fe925ea9 feat: basic chord trainer
fix: don't add chords from backup if identical chords already exist, fixes #30
2023-11-18 18:35:59 +01:00
e84470d577 fix: chord actions not sorted 2023-11-18 15:53:07 +01:00
683561dc06 feat: chord backup import 2023-11-18 11:21:50 +01:00
2fd2dad6f7 fix: chord maps are ordered incorrectly with new chords, fixes #24 2023-11-18 01:53:49 +01:00
e2f9f87b13 fix: very large toggles 2023-11-15 02:19:10 +01:00
623d895aea fix: broken site 2023-11-15 02:05:44 +01:00
561300de64 refactor: cleanup 2023-11-15 01:46:23 +01:00
c5d9defc9d feat: layout url import
feat: backup import (except chords)
feat: legacy layout import
feat: separate layout, chord & setting backup downloads
2023-11-15 01:14:34 +01:00
acd58646f6 feat: add generid 103-key layout for CCX users, fixes #12 2023-11-14 23:37:06 +01:00
3634264af3 fix: chentry disables unrelated settings, fixes #8 2023-11-14 23:03:34 +01:00
3515994a5a fix: settings page doesn't let you input more than one number, fixes #5 2023-11-14 23:00:18 +01:00
bdebe238ae feat: auto-show connect dialog when auto-connect is disabled, resolves #14 2023-11-14 22:51:59 +01:00
ebf7d73d20 feat: new blocking progress bar, fixes #18
feat: change cloud icon to history, fixes #15
fix: action search items overlap, fixes #16
feat: show tooltips immediately
2023-11-14 20:19:01 +01:00
e19a57efac feat: new chord button, fixes #9
feat: improved backups
2023-11-10 17:31:52 +01:00
034436f93e fix: editing chords messes up list 2023-11-10 16:05:42 +01:00
2710f7fc25 fix: phrase insert button not working 2023-11-10 16:00:34 +01:00
d2276a53d0 feat: chord editing 2023-11-10 15:45:04 +01:00
8701d7a40d fix: strikethrough not showing 2023-11-10 01:30:55 +01:00
94cfaf40e5 feat: new chord editing
feat: clear all changes with shift undo, fixes #7
2023-11-10 01:17:36 +01:00
c661a4b30b fix: chords can't be deleted 2023-11-03 23:13:56 +01:00
9b95e1d67a refactor: update branding
Fixes #4
2023-11-03 22:45:30 +01:00
f7bf93fcfc feat: chord editing prototype
feat: printing style for layout
2023-11-03 22:37:27 +01:00
08df049170 feat: rudimentary filter in action selector
Fixes #1
2023-11-03 18:57:22 +01:00
65a536cdea fix: chord deletion outputs empty string
Fixes #3
2023-11-03 18:26:58 +01:00
d2fd84a6b5 fix: add vendor ids for additional devices
fix: use proper semver parsing for device versions

Fixes #2
2023-11-03 18:24:32 +01:00
88429412b9 fix: lite breaks layout viewer 2023-11-02 22:13:37 +01:00
ef309d603e feat: editing 2023-11-02 00:16:18 +01:00
fade2f978e fix: cc1 layout keys 2023-11-01 17:03:38 +01:00
a1760d518c fix: layout keys 2023-11-01 16:40:18 +01:00
9d33565081 fix: search page pagination 2023-11-01 16:24:29 +01:00
Raymond Li
11fe12f095 Put CNAME into build.yml 2023-10-31 18:36:13 -04:00
aba390839b add cc1 visual layout 2023-10-31 23:27:44 +01:00
Raymond Li
a6e7df55ff Disable jekyll in build.yml 2023-10-31 18:12:16 -04:00
Raymond Li
7e5e7b8f5f Update build.yml 2023-10-31 17:50:43 -04:00
Raymond Li
a34ba35889 Remove extra checkout in build.yml 2023-10-31 17:49:54 -04:00
Raymond Li
616d15b6bd Merge jobs in build.yml 2023-10-31 17:48:59 -04:00
Raymond Li
283444f0be Fix build.yml 2023-10-31 17:44:31 -04:00
Raymond Li
e5e56c04a2 Update build.yml to deploy to GitHub pages 2023-10-31 17:34:10 -04:00
Raymond Li
a34c176bcc Update README.md to official CharaChorder implementation 2023-10-31 17:22:44 -04:00
e4d51cd51d visual layout adjustments 2023-10-31 22:09:33 +01:00
a7b49de6ac lite layout 2023-10-31 18:22:03 +01:00
fc86b31337 feat: chord editing prototype
feat: lazy device connections
feat: backup docs
feat: chord library pagination
2023-10-27 19:39:26 +02:00
d8f0679233 keyboard stuff, styling things 2023-09-25 18:12:34 +02:00
c93246ee8c 0.6.5 2023-09-22 20:51:42 +02:00
22905c2b56 0.6.4 2023-09-22 20:49:29 +02:00
074f1da48d update version hook 2023-09-22 20:49:29 +02:00
e7a52221d2 feat: layout editing (sorta) 2023-09-22 20:27:15 +02:00
f03b4d586b feat: version and issue url 2023-09-22 14:15:01 +02:00
4cd9ce536d feat: new sharing system
feat: support legacy layout import
2023-09-16 14:17:59 +02:00
a39f57bac1 feat: apply setting changes and add commit feature 2023-09-07 17:39:33 +02:00
bf96c1e29d feat: include dev tools in releases 2023-08-04 22:38:18 +02:00
a134b970ee fix: windows build broke 2023-08-04 21:35:02 +02:00
86476cfdd8 fix: tauri update server 2023-08-04 01:40:35 +02:00
742e7a6b98 fix: tauri build dependencies 2023-08-04 01:11:21 +02:00
607338878b fix: tauri build dependencies 2023-08-04 01:10:43 +02:00
777488ecd1 fix: tauri build dependencies 2023-08-04 00:38:25 +02:00
220c8cbe67 fix: tauri build 2023-08-04 00:29:14 +02:00
42922e7ce0 feat: tauri serial polyfill 2023-08-04 00:08:28 +02:00
9c1918e683 feat: tauri serial polyfill 2023-08-03 00:27:03 +02:00
5014e1e8e8 feat: tauri testing 2023-08-02 22:07:13 +02:00
e0f5c6440c feat: tauri testing 2023-08-02 22:04:20 +02:00
e21ff12804 feat: tauri testing 2023-08-02 21:55:10 +02:00
2fa8d93d60 feat: tauri testing 2023-08-02 21:49:48 +02:00
aa1d4787f5 feat: code sandbox
[deploy]
2023-08-01 02:09:27 +02:00
4cc9462655 feat: de-clutter navbar
fix: backup option not working
refactor: persistent writable stores

[deploy]
2023-07-29 22:50:18 +02:00
7d148d0c2c feat: 3d click in layout
feat: action autocomplete

[deploy]
2023-07-29 17:31:14 +02:00
73c71836dc fix: patch flexsearch type definitions
[deploy]
2023-07-28 19:56:00 +02:00
e508d1312e fix: patch flexsearch type definitions
[deploy]
2023-07-28 18:54:02 +02:00
c709878d6a fix: use proper phrase decompress algorithm
[deploy]
2023-07-28 17:04:43 +02:00
374e27c7d0 feat: i18n
[deploy]
2023-07-26 23:45:11 +02:00
88c7f057c9 feat: i18n 2023-07-26 23:41:13 +02:00
6b09cbfbec feat: i18n 2023-07-26 21:51:17 +02:00
06c1121983 feat: settings readout 2023-07-25 19:42:18 +02:00
2130b6c7b9 feat: settings wip 2023-07-24 22:58:10 +02:00
e64082d578 feat: complete device serial api implementation 2023-07-24 21:04:42 +02:00
21dbfa48de feat: layout action search prototype
[deploy]
2023-07-24 00:37:45 +02:00
7df75e109d feat: user themes
[deploy]
2023-07-23 23:01:21 +02:00
5cdf969c6d feat: improve connection ux
[deploy]
2023-07-23 21:29:54 +02:00
634073f10d feat: new connection flow
[deploy]
2023-07-23 17:45:07 +02:00
4cc3343984 feat: new connection flow 2023-07-23 17:44:26 +02:00
998a400395 stuff 2023-07-23 00:43:54 +02:00
c0fb737314 add nix development flake 2023-07-21 00:14:37 +02:00
c59b2732f7 add nix development flake 2023-07-21 00:07:11 +02:00
9bf1a13e02 change name to amacc1ng
[deploy]
2023-07-18 02:03:46 +02:00
6facaad4a2 add layout edit placeholders
[deploy]
2023-07-18 02:01:25 +02:00
b04ed7fe7f add tooltip stuff
[deploy]
2023-07-18 01:40:30 +02:00
4eb1e8c049 add browser warning 2023-07-17 18:44:18 +02:00
26ca9984ea typing challenge prototype 2023-07-10 20:33:43 +02:00
110771a2a4 typing challenge prototype 2023-07-10 19:55:58 +02:00
7fdf1cd3b4 replace . base64 character with ~ because discord things a . in the end is not part of the url
[deploy]
2023-07-09 01:47:16 +02:00
c4fee59446 Disable SPA redirect for now 2023-07-09 01:38:57 +02:00
088aa0dbcf update .htaccess to handle query params
[deploy]
2023-07-09 01:30:58 +02:00
26a6f70ccb layout sharing via url
[deploy]
2023-07-09 01:20:38 +02:00
391c9d8837 layout sharing via url
[deploy]
2023-07-08 23:19:58 +02:00
3a167030da improve backups
[deploy]
2023-07-08 18:45:51 +02:00
e38e63222c pipeline
[deploy]
2023-07-08 18:20:40 +02:00
7c74831647 pipeline
[deploy]
2023-07-08 18:18:46 +02:00
264 changed files with 29043 additions and 11561 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -2,65 +2,56 @@ name: Build
on:
push:
branches: [ "master" ]
tags: ["v*"]
branches:
- master
tags:
- v*
pull_request:
branches: [ "master" ]
jobs:
build:
name: 🔨 Build
name: 🔨🚀 Build and deploy
runs-on: ubuntu-latest
steps:
- name: 🚚 Checkout
uses: actions/checkout@v3
- name: 🐍 Use Python 3.x
uses: actions/setup-python@v3.1.4
with:
python-version: 3.x
cache: pip
- name: ⏬ Install Python dependencies
run: python -m venv venv
- run: ./venv/bin/pip install -r requirements.txt
- name: 🐉 Use Node.js 18.16.x
uses: actions/setup-node@v3
with:
node-version: 18.16.x
cache: 'npm'
- name: ⏬ Install Node dependencies
run: npm ci
- name: 🚚 Checkout
uses: actions/checkout@v4
- name: 🐍 Use Python 3.x
uses: actions/setup-python@v5
with:
python-version: 3.x
cache: pip
- name: ⏬ Install Python dependencies
run: pip install -r requirements.txt
- name: 🔥 Optimize icon font
run: npm run minify-icons
- name: 🔨 Build site
run: npm run build
- name: 📦 Upload build artifacts
uses: actions/upload-artifact@v3.1.2
with:
name: build
path: build
deploy:
name: 🚀 Deploy
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: build
environment:
name: Website
url: https://dotio.theaninova.de
steps:
- name: 📦 Download build artifacts
uses: actions/download-artifact@v2.1.1
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
name: build
path: build
- name: 🚀 Deploy
uses: SamKirkland/web-deploy@v1
version: 10
- name: 🐉 Use Node.js 22.14.x
uses: actions/setup-node@v4
with:
target-server: ${{ secrets.SSH_SERVER }}
destination-path: ~/public_html/
source-path: ./build/
remote-user: ${{ secrets.SSH_USER }}
private-ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-port: ${{ secrets.SSH_PORT }}
node-version: 22.14.x
cache: "pnpm"
- name: ⏬ Install Node dependencies
run: pnpm install
- name: 🔥 Optimize icon font
run: pnpm minify-icons
- name: 🔨 Build site
run: pnpm build
- name: Setup SSH
run: |
install -m 600 -D /dev/null ~/.ssh/id_rsa
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_rsa
echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- name: Publish Stable
if: ${{ github.ref == 'refs/tags/v*' && !github.event.pull_request.head.repo.fork }}
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/www/
- name: Publish Branch
if: ${{ !github.event.pull_request.head.repo.fork }}
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${GITHUB_REF##*/}
- name: Publish Commit
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${{ github.sha }}

54
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: "publish desktop apps"
on:
push:
tags:
- "desktop-app-v*"
workflow_dispatch:
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-20.04, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: 🚚 Checkout
uses: actions/checkout@v3
- name: 🐍 Use Python 3.x
uses: actions/setup-python@v3.1.4
with:
python-version: 3.x
cache: pip
- name: ⏬ Install Python dependencies
run: pip install -r requirements.txt
- name: 🐉 Use Node.js 18.16.x
uses: actions/setup-node@v3
with:
node-version: 18.16.x
cache: "npm"
- name: 🦀 Use Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: 🐧 Install Linux Dependencies
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libudev-dev libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: ⏬ Install Node dependencies
run: npm ci
- name: 🔥 Optimize icon font
run: npm run minify-icons
- name: 📦 Build, Package & Release
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
with:
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
releaseName: "App v__VERSION__"
releaseBody: "See the assets to download this version and install."
releaseDraft: true
prerelease: false

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ node_modules
/package
.env
.env.*
.direnv
!.env.example
venv
vite.config.js.timestamp-*

View File

@@ -6,8 +6,13 @@ node_modules
.env
.env.*
!.env.example
/src-tauri/target
/openssl*
/src/i18n/i18n*
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
static/languages/*.json

View File

@@ -1,9 +1,4 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{"files": "*.svelte", "options": {"parser": "svelte"}}]
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

5
.typesafe-i18n.json Normal file
View File

@@ -0,0 +1,5 @@
{
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
"baseLocale": "en",
"adapter": "svelte"
}

43
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,43 @@
# Contributing
## UX Principles
- **Opinionated.** There should never be two ways to do the same thing.
- **Intuitive.** If a feature needs a description to explain it,
the feature has failed.
- **Simple.** No useless buttons that always need to be pressed.
## UI Design
The UI design is based on Material 3.
## Development Setup
### Nix
[Enable flakes](https://nixos.wiki/wiki/Flakes#Enable_flakes), then start the development shell using
```shell
nix develop
```
You may need to run through some additional setup to get Rust running inside IntelliJ.
### Other platforms
- NodeJS >=18.16
- Python >=3.10
- Rust Stable (For Tauri Development)
I know, python in JS projects is extremely annoying. Unfortunately,
it seems to be the only platform that offers a functional
way to subset variable woff2 fonts with ligatures.
In other words, either have python as a development dependency or
serve a 3.5MB icons font of which 99.5% is completely unused.
To generate the icons use the following command:
```shell
npm run minify-icons
```

View File

@@ -1,25 +1,12 @@
# dot i/o V2
# CharaChorder Device Manager
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/Theaninova/dotio/build.yml)
![GitHub](https://img.shields.io/github/license/Theaninova/dotio)
[![GitHub deployments](https://img.shields.io/github/deployments/Theaninova/dotio/Website?label=delployment)](https://dotio.theaninova.de/)
The official device manager and configuration tool for CharaChorder devices.
_This project is not affiliated or endorsed with neither the original [dot i/o](https://www.iq-eq.io/) site, nor [CharaChorder](https://www.charachorder.com/)_
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/CharaChorder/DeviceManager/build.yml)
![GitHub](https://img.shields.io/github/license/CharaChorder/DeviceManager)
[![GitHub deployments](https://img.shields.io/github/deployments/CharaChorder/DeviceManager/Website?label=delployment)](https://manager.charachorder.com/)
I aim to create a new site that offers an easier, visually pleasing
and more complete way to configure and learn CharaChorder devices.
## Development
- NodeJS >=18.16
- Python >=3.10 virtual environment
I know, python in JS projects is extremely annoying, unfortunately,
it seems to be the only platform that offers a functional
way to subset variable woff2 fonts with ligatures.
In other words, either have python as a development dependency or
serve a 3.5MB icons font of which 99.5% is completely unused.
Get the latest desktop release [here](https://github.com/CharaChorder/DeviceManager/releases).
## Deployment
@@ -42,3 +29,11 @@ To double-check, make sure your private key starts with
After that, add the `SSH_SERVER`, `SSH_PORT`, `SSH_PRIVATE_KEY` and `SSH_USER`
environment secrets to your environment in GitHub.
## Releases
Change the version in
- [package.json](package.json)
- [tauri.conf.json](src-tauri/tauri.conf.json)
- [Cargo.toml](src-tauri/Cargo.toml)

64
docs/BACKUP.md Normal file
View File

@@ -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.

96
flake.lock generated Normal file
View File

@@ -0,0 +1,96 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1743259260,
"narHash": "sha256-ArWLUgRm1tKHiqlhnymyVqi5kLNCK5ghvm06mfCl4QY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "eb0e0f21f15c559d2ac7633dc81d079d1caf5f5f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1736320768,
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1743388531,
"narHash": "sha256-OBcNE+2/TD1AMgq8HKMotSQF8ZPJEFGZdRoBJ7t/HIc=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "011de3c895927300651d9c2cb8e062adf17aa665",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

84
flake.nix Normal file
View File

@@ -0,0 +1,84 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
flake-utils,
rust-overlay,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
overlays = [
(import rust-overlay)
(final: prev: {
nodejs = prev.nodejs_22;
corepack = prev.corepack_22;
})
];
pkgs = import nixpkgs { inherit system overlays; };
rust-bin = pkgs.rust-bin.stable.latest.default.override {
extensions = [
"rust-src"
"rust-std"
"clippy"
"rust-analyzer"
];
};
fontMin = pkgs.python311.withPackages (
ps:
with ps;
[
brotli
fonttools
]
++ (with fonttools.optional-dependencies; [ woff ])
);
tauriPkgs = nixpkgs.legacyPackages.${system};
libraries = with tauriPkgs; [
webkitgtk
gtk3
cairo
gdk-pixbuf
glib
dbus
openssl_3
librsvg
];
packages =
(with pkgs; [
nodejs
pnpm
rust-bin
fontMin
])
++ (with tauriPkgs; [
curl
wget
pkg-config
dbus
openssl_3
glib
gtk3
libsoup_2_4
webkitgtk
librsvg
# serial plugin
udev
]);
in
{
devShell = pkgs.mkShell {
buildInputs = packages;
shellHook = ''
#export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
'';
};
}
);
}

163
icons.config.js Normal file
View File

@@ -0,0 +1,163 @@
/** @type {import('./src/tools/icons-config').IconsConfig} */
const config = {
inputPath:
"node_modules/@fontsource-variable/material-symbols-rounded/files/material-symbols-rounded-latin-full-normal.woff2",
outputPath: "src/lib/assets/icons.min.woff2",
icons: [
"rocket_launch",
"deployed_code_update",
"adjust",
"add",
"piano",
"keyboard",
"settings",
"edit",
"music_note",
"avg_pace",
"lyrics",
"speed",
"cognition",
"update",
"offline_pin",
"warning",
"dangerous",
"check",
"cable",
"person",
"sync",
"school",
"restart_alt",
"usb",
"usb_off",
"rule_settings",
"123",
"abc",
"function",
"cloud_done",
"backup",
"cloud_download",
"cloud_off",
"share",
"ios_share",
"close",
"arrow_back",
"arrow_back_ios_new",
"save",
"settings_backup_restore",
"sound_detection_loud_sound",
"ring_volume",
"wifi",
"power_settings_circle",
"graphic_eq",
"mail",
"calculate",
"open_in_browser",
"chevron_backward",
"chevron_forward",
"bookmark",
"drag_pan",
"markdown_copy",
"sort",
"shopping_bag",
"filter_list",
"settings_power",
"link",
"link_off",
"chevron_right",
"check_circle",
"error",
"auto_delete",
"format_paint",
"dark_mode",
"light_mode",
"palette",
"translate",
"play_arrow",
"extension",
"upload_file",
"commit",
"bug_report",
"delete",
"remove_selection",
"bolt",
"thunderstorm",
"join_inner",
"uppercase",
"undo",
"redo",
"replay",
"reply",
"navigate_before",
"navigate_next",
"library_add",
"reset_wrench",
"reset_settings",
"delete_sweep",
"print",
"restore_from_trash",
"history",
"history_toggle_off",
"text_to_speech",
"sentiment_satisfied",
"sentiment_dissatisfied",
"sentiment_very_satisfied",
"sentiment_neutral",
"sentiment_very_dissatisfied",
"sentiment_excited",
"sentiment_frustrated",
"sentiment_calm",
"sentiment_stressed",
"sentiment_extremely_dissatisfied",
"sentiment_sad",
"sentiment_content",
"sentiment_worried",
"construction",
"timer",
"target",
"download",
"download_2",
"upload_2",
"stat_minus_2",
"stat_2",
"send",
"more_horiz",
"add_reaction",
"stop",
"description",
"add_circle",
"refresh",
"tune",
"edit_document",
"chat",
"account_circle",
"experiment",
"code",
"dictionary",
"developer_board",
"developer_board_off",
"memory",
],
codePoints: {
speed: "e9e4",
arrow_split: "e985",
arrow_circle_down: "f181",
arrow_circle_up: "f182",
counter_1: "f784",
counter_2: "f783",
counter_3: "f782",
ios_share: "e6b8",
light_mode: "e518",
upload_file: "e9fc",
no_sound: "e710",
sentiment_extremely_dissatisfied: "f194",
download_2: "f523",
upload_2: "ff52",
stat_minus_2: "e69c",
stat_2: "e699",
routine: "e20c",
experiment: "e686",
dictionary: "f539",
},
};
export default config;

View File

@@ -1,56 +0,0 @@
export interface IconsConfig {
codePoints: Record<string, string>
inputPath: string
outputPath: string
icons: string[]
}
const config: IconsConfig = {
inputPath:
"node_modules/@fontsource-variable/material-symbols-rounded/files/material-symbols-rounded-latin-full-normal.woff2",
outputPath: "src/lib/assets/icons.min.woff2",
icons: [
"piano",
"keyboard",
"settings",
"music_note",
"avg_pace",
"lyrics",
"speed",
"cognition",
"update",
"offline_pin",
"warning",
"cable",
"person",
"sync",
"restart_alt",
"usb",
"rule_settings",
"123",
"abc",
"function",
"cloud_done",
"backup",
"cloud_download",
"share",
"ios_share",
"close",
"arrow_back",
"arrow_back_ios_new",
"save",
"settings_backup_restore",
],
codePoints: {
speed: "e9e4",
arrow_split: "e985",
arrow_circle_down: "f181",
arrow_circle_up: "f182",
counter_1: "f784",
counter_2: "f783",
counter_3: "f782",
ios_share: "e6b8",
},
}
export default config

9030
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +1,96 @@
{
"name": "cccs",
"version": "0.1.0",
"name": "charachorder-device-manager",
"version": "2.3.0",
"license": "AGPL-3.0-or-later",
"private": true,
"engines": {
"node": ">=22.14",
"pnpm": ">=10.7"
},
"repository": {
"type": "git",
"url": "https://github.com/CharaChorder/DeviceManager.git"
},
"homepage": "https://docs.charachorder.com",
"bugs": {
"url": "https://github.com/CharaChorder/DeviceManager/issues"
},
"scripts": {
"dev": "vite dev",
"build": "vite build",
"dev": "npm-run-all --parallel vite typesafe-i18n",
"dev:external": "npm-run-all --parallel vite:external typesafe-i18n",
"dev:tauri": "tauri dev",
"vite": "vite dev",
"vite:external": "vite --host",
"build": "typesafe-i18n --no-watch && vite build",
"build:tauri": "tauri build",
"tauri": "tauri",
"test": "vitest run --coverage",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"minify-icons": "ts-node-esm src/tools/minify-icon-font.ts",
"lint": "prettier --plugin-search-dir . --check .",
"format": "prettier --plugin-search-dir . --write ."
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"minify-icons": "node src/tools/minify-icon-font.js",
"version": "node src/tools/version.js && git add src-tauri/Cargo.toml && git add src-tauri/tauri.conf.json",
"lint": "prettier --check .",
"format": "prettier --write .",
"typesafe-i18n": "typesafe-i18n"
},
"devDependencies": {
"@theaninova/prettier-config": "^1.0.0",
"@types/w3c-web-serial": "^1.0.3",
"@vite-pwa/sveltekit": "^0.2.5",
"@fontsource-variable/noto-sans-mono": "^5.0.4",
"@fontsource-variable/material-symbols-rounded": "^5.0.4",
"stylelint": "^15.9.0",
"stylelint-config-standard-scss": "^10.0.0",
"stylelint-config-prettier-scss": "^1.0.0",
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/language": "^6.11.0",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.5",
"@fontsource-variable/material-symbols-rounded": "^5.2.8",
"@fontsource-variable/noto-sans-mono": "^5.2.6",
"@lezer/highlight": "^1.2.1",
"@material/material-color-utilities": "^0.3.0",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.86.6",
"@modyfi/vite-plugin-yaml": "^1.1.1",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.20.2",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tauri-apps/api": "^1.6.0",
"@tauri-apps/cli": "^1.6.0",
"@types/dom-view-transitions": "^1.0.6",
"@types/w3c-web-serial": "^1.0.8",
"@types/w3c-web-usb": "^1.0.10",
"@types/wicg-file-system-access": "^2023.10.5",
"@vite-pwa/sveltekit": "^1.0.0",
"autoprefixer": "^10.4.21",
"codemirror": "^6.0.1",
"cypress": "^14.2.1",
"d3": "^7.9.0",
"esptool-js": "^0.5.4",
"flexsearch": "^0.8.147",
"fontkit": "^2.0.4",
"glob": "^11.0.1",
"jsdom": "^26.0.0",
"matrix-js-sdk": "^37.2.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"rxjs": "^7.8.2",
"sass": "^1.86.0",
"socket.io-client": "^4.8.1",
"stylelint": "^16.17.0",
"stylelint-config-clean-order": "^7.0.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended-scss": "^12.0.0",
"stylelint-config-clean-order": "^5.0.1",
"glob": "^10.3.1",
"flexsearch": "^0.7.31",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.20.4",
"@sveltejs/vite-plugin-svelte": "^2.4.2",
"jsdom": "^22.1.0",
"@material/material-color-utilities": "^0.2.7",
"fontkit": "^2.0.2",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.0",
"svelte-check": "^3.4.3",
"ts-node": "^10.9.1",
"typescript": "^5.0.0",
"vitest": "^0.33.0",
"vite": "^4.3.6",
"vite-plugin-pwa": "^0.16.4",
"@modyfi/vite-plugin-yaml": "^1.0.4",
"svelte-preprocess": "^5.0.4",
"autoprefixer": "^10.4.14",
"sass": "^1.63.6"
"stylelint-config-prettier-scss": "^1.0.0",
"stylelint-config-recommended-scss": "^14.1.0",
"stylelint-config-standard-scss": "^14.0.0",
"svelte": "5.25.3",
"svelte-check": "^4.1.5",
"svelte-preprocess": "^6.0.3",
"tippy.js": "^6.3.7",
"typesafe-i18n": "^5.26.2",
"typescript": "^5.8.2",
"vite": "^6.2.4",
"vite-plugin-mkcert": "^1.17.8",
"vite-plugin-pwa": "^1.0.0",
"vitest": "^3.1.1",
"web-serial-polyfill": "^1.0.15",
"workbox-window": "^7.3.0"
},
"type": "module",
"prettier": "@theaninova/prettier-config"
"type": "module"
}

8644
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/

4075
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

27
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "app"
version = "2.3.0"
description = "A Tauri App"
authors = ["Thea Schöbl <dev@theaninova.de>"]
license = "AGPL-3"
repository = "https://github.com/Theaninova/dotio"
default-run = "app"
edition = "2021"
rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.4.0", features = [] }
[dependencies]
serde_json = "1.0"
serialport = "4.2.1"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4.0", features = ["updater", "devtools"] }
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

11
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,11 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod serial;
fn main() {
tauri::Builder::default()
.plugin(serial::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

99
src-tauri/src/serial.rs Normal file
View File

@@ -0,0 +1,99 @@
use serde::Serialize;
use serialport::{available_ports, SerialPort, SerialPortType};
use std::collections::HashMap;
use std::io::{Read, Write};
use std::sync::{Arc, Mutex};
use tauri::plugin::{Builder, TauriPlugin};
use tauri::{command, generate_handler, Manager, Runtime, State};
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("serial")
.invoke_handler(generate_handler![
get_serial_ports,
open,
close,
read,
write
])
.setup(move |app_handle| {
app_handle.manage(SerialState::default());
Ok(())
})
.build()
}
#[derive(Default)]
pub struct SerialState {
handles: Arc<Mutex<HashMap<String, Box<dyn SerialPort>>>>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct WebSerialPortInfo {
pub name: String,
pub usb_product_id: u16,
pub usb_vendor_id: u16,
pub serial_number: Option<String>,
pub manufacturer: Option<String>,
pub product: Option<String>,
}
#[command]
fn get_serial_ports() -> Result<Vec<WebSerialPortInfo>, String> {
Ok(available_ports()
.map_err(|err| err.to_string())?
.iter()
.filter_map(|port| match &port.port_type {
SerialPortType::UsbPort(usb) => Some(WebSerialPortInfo {
name: port.port_name.clone(),
usb_vendor_id: usb.vid,
usb_product_id: usb.pid,
serial_number: usb.serial_number.clone(),
manufacturer: usb.manufacturer.clone(),
product: usb.product.clone(),
}),
_ => None,
})
.collect())
}
#[command]
fn open(state: State<'_, SerialState>, path: String, baud_rate: u32) -> Result<(), String> {
let mut handles = state.handles.lock().map_err(|err| err.to_string())?;
if handles.contains_key(&path) {
return Ok(());
}
let port = serialport::new(path.clone(), baud_rate)
.open()
.map_err(|err| err.to_string())?;
handles.insert(path, port);
Ok(())
}
#[command]
fn close(state: State<'_, SerialState>, path: String) -> Result<(), String> {
let mut handles = state.handles.lock().map_err(|err| err.to_string())?;
handles.remove(&path).ok_or("Port is already closed")?;
Ok(())
}
#[command]
fn read(state: State<'_, SerialState>, path: String) -> Result<Vec<u8>, String> {
let mut handles = state.handles.lock().map_err(|err| err.to_string())?;
let port = handles.get_mut(&path).ok_or("Read: Port is not open")?;
let size = port.bytes_to_read().map_err(|err| err.to_string())?;
let mut buffer: Vec<u8> = vec![0; size as usize];
port.read_exact(buffer.as_mut_slice())
.map_err(|err| err.to_string())?;
Ok(buffer)
}
#[command]
fn write(state: State<'_, SerialState>, path: String, chunk: Vec<u8>) -> Result<(), String> {
let mut handles = state.handles.lock().map_err(|err| err.to_string())?;
let port = handles.get_mut(&path).ok_or("Write: Port is not open")?;
port.write_all(&chunk).map_err(|err| err.to_string())
}

63
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,63 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"devPath": "http://localhost:5173",
"distDir": "../build"
},
"package": { "productName": "amacc1ng", "version": "2.3.0" },
"tauri": {
"allowlist": { "all": false },
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "AGPL-3.0-or-later",
"deb": { "depends": [] },
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "de.theaninova.chara-app",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": { "csp": null },
"updater": {
"active": true,
"endpoints": [
"https://amacc1ng.theaninova.de/update?current_version={{current_version}}&target={{target}}&arch={{arch}}",
"https://dotio.theaninova.de/update?current_version={{current_version}}&target={{target}}&arch={{arch}}"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDU5QjEwMEY5RjNBRjM4MEIKUldRTE9LL3orUUN4V2FMWDZkc2l2VUdOL3FSdUMwTk1ualNac095RVZXVEpqUEtORkFsWGZaTmsK"
},
"windows": [
{
"fullscreen": false,
"height": 720,
"resizable": true,
"title": "amacc1ng",
"width": 1280
}
]
}
}

2
src/app.d.ts vendored
View File

@@ -11,4 +11,4 @@ declare global {
}
}
export {}
export {};

View File

@@ -1,8 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="icon" href="%sveltekit.assets%/icon.svg" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>

23
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
/// <references types="vite/client" />
interface ImportMetaEnv {
readonly TAURI_FAMILY?: string;
readonly TAURI_PLATFORM_VERSION?: string;
readonly TAURI_TARGET_TRIPLE?: string;
readonly TAURI_ARCH?: string;
readonly TAURI_DEBUG?: boolean;
readonly TAURI_PLATFORM_TYPE?: string;
readonly VITE_HOMEPAGE_URL: string;
readonly VITE_BUGS_URL: string;
readonly VITE_DOCS_URL: string;
readonly VITE_LEARN_URL: string;
readonly VITE_LATEST_FIRMWARE: string;
readonly VITE_STORE_URL: string;
readonly VITE_MATRIX_URL: string;
readonly VITE_FIRMWARE_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

1
src/i18n/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
i18n-*.ts

144
src/i18n/de/index.ts Normal file
View File

@@ -0,0 +1,144 @@
import type { Translation } from "../i18n-types";
const de = {
TITLE: "CharaChorder Gerätemanager",
DESCRIPTION: "Gerätemanager und Konfigurationstool für CharaChorder Geräte.",
saveActions: {
UNDO: "Rückgängig (<kbd class='icon'>shift</kbd> halten um alle Änderungen rückgängig zu machen)",
REDO: "Wiederholen",
SAVE: "Speichern",
},
update: {
TITLE: "Gerät aktualisieren",
},
sync: {
TITLE_READ: "Neueste Änderungen werden abgerufen",
TITLE_WRITE: "Änderungen werden gespeichert",
RELOAD: "Neu laden",
},
backup: {
TITLE: "Backup",
AUTO_BACKUP: "Auto-backup",
DISCLAIMER:
"Das Backup in diesem Browser gespeichert und bleibt nur auf diesem Computer.",
DOWNLOAD: "Alles",
RESTORE: "Wiederherstellen",
},
modal: {
CLOSE: "Schließen",
},
actionSearch: {
PLACEHOLDER: "Nach Aktionen suchen",
CURRENT_ACTION: "Aktuelle Aktion",
NEXT_ACTION: "Aktion nach dem nächsten Speichern",
DELETE: "Entfernen",
filter: {
ALL: "Alle",
},
LIVE_LAYOUT_INFO:
"Diese Aktion wurde auf Basis des Systemtastaturlayouts ermittelt.",
SHIFT_WARNING: "Diese Aktion hält <kbd class='icon'>shift</kbd>",
ALT_CODE_WARNING: "Dieses Alt-Code Makro funktioniert nur unter Windows",
},
share: {
TITLE: "Teilen",
URL_COPIED: "Teilbare URL kopiert!",
EXTRA_DOWNLOAD: "Als Datei herunterladen",
},
print: {
TITLE: "Drucken",
},
profile: {
TITLE: "Profil",
LANGUAGE: "Sprache",
theme: {
TITLE: "Darstellung",
COLOR_SCHEME: "Farbschema",
DARK_MODE: "Dunkel",
LIGHT_MODE: "Hell",
},
},
deviceManager: {
TITLE: "Gerät",
AUTO_CONNECT: "Automatisch Verbinden",
CONNECT: "Verbinden",
DISCONNECT: "Entfernen",
TERMINAL: "Konsole",
APPLY_SETTINGS: "Änderungen auf das Gerät brennen",
NO_DEVICE: "Kein Gerät verbunden",
LINUX_PERMISSIONS:
"Auf den meisten Linux-basierten Systemen müssen zuerst Berechtigungen angepasst werden. Flatpak und Snap Anwendungen benötigen zusätzliche Berechtigungen oder funktionieren möglicherweise gar nicht.",
bootMenu: {
TITLE: "Bootmenü",
REBOOT: "Neustarten",
BOOTLOADER: "Bootloader",
POWER_WARNING:
"Um vom Bootloader aus neu zu starten muss das Gerät neu verbunden werden.",
},
},
browserWarning: {
TITLE: "Warnung",
INFO_SERIAL_PREFIX:
"Der aktuell genutzte Browser wird aufgrund der speziellen Voraussetzung für Kommunikation über die ",
INFO_SERIAL_INFIX: "serielle Schnittstelle",
INFO_SERIAL_SUFFIX: " nicht unterstützt.",
INFO_BROWSER_PREFIX:
"Auch wenn alle Chromium-basieren Desktop Browser diese Voraussetzung grundsätzlich erfüllen, haben einige Browser ",
INFO_BROWSER_INFIX: "wie zum Beispiel Brave",
INFO_BROWSER_SUFFIX:
" sich bewusst dazu entschieden die API zu deaktivieren.",
DOWNLOAD_APP:
"Chrome oder Edge werden offiziell unterstützt, andere Browser könnten aber auch funktionieren.",
},
changes: {
TITLE: "Änderungen importieren",
ALL_CHANGES: "Alle Änderungen",
layout: {
TITLE: "{0} veränderte Belegung{{:|en}}",
LAYER: "{changes} Belegung{{changes:|en}} in Ebene {layer} ändern",
},
settings: {
TITLE: "{0} Einstellung{{|en}} anpassen",
},
chords: {
TITLE: "{0} Akkorde",
NEW_CHORDS: "{0} neue Akkord{{|e}} hinzufügen",
CHANGED_CHORDS: "{0} Akkord{{|e}} ersetzen",
DELETED_CHORDS: "{0} Akkord{{|e}} zum löschen markieren",
},
},
configure: {
chords: {
TITLE: "Bibliothek",
HOLD_KEYS: "Akkord halten",
NEW_CHORD: "Neuer Akkord",
DUPLICATE: "Akkord existiert bereits",
search: {
PLACEHOLDER: "{0} Akkord{{|e}} durchsuchen",
NO_RESULTS: "Keine Ergebnisse",
},
conflict: {
TITLE: "Akkordkonflikt",
DESCRIPTION:
"Der Akkord würde einen bereits existierenden Akkord überschreiben. Wirklich fortfahren?",
CONFIRM: "Überschreiben",
ABORT: "Überspringen",
},
VOCABULARY: "Vokabelliste",
TRY_TYPING: "Versuche hier zu tippen",
},
layout: {
TITLE: "Layout",
},
settings: {
TITLE: "Gerät",
},
},
plugin: {
editor: {
RUN: "Ausführen",
},
},
} satisfies Translation;
export default de;

143
src/i18n/en/index.ts Normal file
View File

@@ -0,0 +1,143 @@
import type { BaseTranslation } from "../i18n-types";
const en = {
TITLE: "CharaChorder Device Manager",
DESCRIPTION:
"The device manager and configuration tool for CharaChorder devices.",
saveActions: {
UNDO: "Undo (hold <kbd class='icon'>shift</kbd> to undo all changes)",
REDO: "Redo",
SAVE: "Save",
},
update: {
TITLE: "Update your device",
},
backup: {
TITLE: "Backup",
AUTO_BACKUP: "Auto-backup",
DISCLAIMER:
"Whenever you connect this device to browser, a backup is made locally and kept only on your computer.",
DOWNLOAD: "Everything",
RESTORE: "Restore",
},
sync: {
TITLE_READ: "Reading latest changes",
TITLE_WRITE: "Saving changes to device",
RELOAD: "Reload",
},
modal: {
CLOSE: "Close",
},
actionSearch: {
PLACEHOLDER: "Search for actions",
CURRENT_ACTION: "Current action",
NEXT_ACTION: "Action after next save",
DELETE: "Remove",
filter: {
ALL: "All",
},
LIVE_LAYOUT_INFO: "This output was determined using on your system layout.",
SHIFT_WARNING: "This action holds <kbd class='icon'>shift</kbd>",
ALT_CODE_WARNING: "This alt-code macro only works on Windows",
},
share: {
TITLE: "Share",
URL_COPIED: "Sharable URL copied!",
EXTRA_DOWNLOAD: "Download as file",
},
print: {
TITLE: "Print",
},
profile: {
TITLE: "Profile",
LANGUAGE: "Language",
theme: {
TITLE: "Theme",
COLOR_SCHEME: "Color scheme",
DARK_MODE: "Dark",
LIGHT_MODE: "Light",
},
},
deviceManager: {
TITLE: "Device",
AUTO_CONNECT: "Auto-connect",
CONNECT: "Connect",
DISCONNECT: "Disconnect",
TERMINAL: "Terminal",
APPLY_SETTINGS: "Flash changes to device",
NO_DEVICE: "No device connected",
LINUX_PERMISSIONS:
"Most Linux based systems need adjusted permissions in order to connect your device. Flatpak or Snap versions in particular might need additional permissions or may not work at all.",
bootMenu: {
TITLE: "Boot Menu",
REBOOT: "Reboot",
BOOTLOADER: "Bootloader",
POWER_WARNING:
"To reboot from bootloader you need to physically reconnect your device.",
},
},
browserWarning: {
TITLE: "Warning",
INFO_SERIAL_PREFIX:
"Your current browser is not supported due to this site's unique requirement for ",
INFO_SERIAL_INFIX: "serial connections",
INFO_SERIAL_SUFFIX: ".",
INFO_BROWSER_PREFIX:
"Though all chromium-based desktop browsers fulfill this requirement, some derivations such as Brave ",
INFO_BROWSER_INFIX: "have been known to disable the API intentionally",
INFO_BROWSER_SUFFIX: ".",
DOWNLOAD_APP:
"Chrome or Edge are officially supported, but other browsers might work as well.",
},
changes: {
TITLE: "Import changes",
ALL_CHANGES: "All changes",
layout: {
TITLE: "{0} layout change{{|s}}",
LAYER: "Update {changes} key{{changes:|s}} in layer {layer}",
},
settings: {
TITLE: "Update {0} setting{{|s}}",
},
chords: {
TITLE: "{0} chords",
NEW_CHORDS: "Add {0} new chord{{|s}}",
CHANGED_CHORDS: "Replace {0} chord{{|s}}",
DELETED_CHORDS: "Mark {0} chord{{|s}} for deletion",
},
},
configure: {
chords: {
TITLE: "Library",
HOLD_KEYS: "Hold chord",
NEW_CHORD: "New chord",
DUPLICATE: "Chord already exists",
search: {
PLACEHOLDER: "Search {0} chord{{|s}}",
NO_RESULTS: "No results",
},
conflict: {
TITLE: "Chord conflict",
DESCRIPTION:
"Your chord conflicts with an existing chord. Are you sure you want to overwrite this chord?",
CONFIRM: "Overwrite",
ABORT: "Skip",
},
VOCABULARY: "Vocabulary",
TRY_TYPING: "Try typing here",
},
layout: {
TITLE: "Layout",
},
settings: {
TITLE: "Device",
},
},
plugin: {
editor: {
RUN: "Run",
},
},
} satisfies BaseTranslation;
export default en;

12
src/i18n/formatters.ts Normal file
View File

@@ -0,0 +1,12 @@
import type { FormattersInitializer } from "typesafe-i18n";
import type { Locales, Formatters } from "./i18n-types";
export const initFormatters: FormattersInitializer<Locales, Formatters> = (
_locale: Locales,
) => {
const formatters: Formatters = {
// add your formatter functions here
};
return formatters;
};

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import { fly } from "svelte/transition";
import { afterNavigate, beforeNavigate } from "$app/navigation";
import { expoIn, expoOut } from "svelte/easing";
import type { Snippet } from "svelte";
let { children, routeOrder }: { children: Snippet; routeOrder: string[] } =
$props();
let inDirection = $state(0);
let outDirection = $state(0);
let outroEnd: undefined | (() => void) = $state(undefined);
let animationDone: Promise<void>;
let isNavigating = $state(false);
function routeIndex(route: string | undefined): number {
return routeOrder.findIndex((it) => route?.startsWith(it));
}
beforeNavigate((navigation) => {
const from = routeIndex(navigation.from?.url.pathname);
const to = routeIndex(navigation.to?.url.pathname);
if (from === -1 || to === -1 || from === to) return;
isNavigating = true;
inDirection = from > to ? -1 : 1;
outDirection = from > to ? 1 : -1;
animationDone = new Promise((resolve) => {
outroEnd = resolve;
});
});
afterNavigate(async () => {
await animationDone;
isNavigating = false;
});
</script>
{#if !isNavigating}
<main
in:fly={{ y: inDirection * 24, duration: 150, easing: expoOut }}
out:fly={{ y: outDirection * 24, duration: 150, easing: expoIn }}
onoutroend={outroEnd}
>
{@render children()}
</main>
{/if}
<style lang="scss">
main {
padding: 0;
}
</style>

View File

@@ -1,497 +0,0 @@
s + Dup,say
y + b,by
y + Dup,why
n + Dup,no
n + f,find
l + f,life
l + p,people
l + s + p,spell
l + n,line
l + n + p,plant
t + h,that
t + n + h,than
t + n + p,plant
t + l + f,left
a + f,after
a + d,add
a + d + f,had
a + h,has
a + s,as
a + s + h,has
a + y + d,day
a + y + s,say
a + n,an
a + n + d,and
a + n + d + f,hand
a + n + h,hand
a + n + y,any
a + l,all
a + l + d,land
a + l + p,plant
a + l + s,last
a + l + y + p,play
a + l + n + d,land
a + t,at
a + t + h,that
a + t + n + h,than
a + t + l + s,last
a + t + l + n + p,plant
- + ?,question
w + h,who
w + s,saw
w + y + h,why
w + t,without
w + t + h,watch
w + t + n,went
w + a,was
w + a + h,what
w + a + s,saw
w + a + y,way
w + a + y + Dup,away
w + a + n,want
w + a + l + f,walk
w + a + l + y,always
w + a + l + y + s,always
w + a + t,watch
w + a + t + h,watch
w + a + t + n,want
g + b,begin
g + Dup,question
g + h,here
g + p,page
g + l + n,long
g + t,get
k + q,quick
k + f,and
k + l + y + q,quickly
k + t + l,talk
k + a + b,back
k + a + s,ask
k + a + t + l,talk
k + w + a + l,walk
m + f,form
m + y,my
m + t + Dup,mountain
m + a + f,family
m + a + s,small
m + a + y,may
m + a + n,man
m + a + n + y,many
m + a + l,almost
m + a + l + s,small
m + a + l + s + Dup,small
c + b,because
c + Dup,sea
c + h,head
c + a + n,can
c + a + n + h,change
c + a + l,call
c + a + l + p,place
c + k + a + b,back
u + p,us
u + y,you
u + j,just
u + j + s,just
u + t + b,but
u + t + p,put
u + t + s + d,study
u + t + n,until
u + t + j + s,just
u + a + l,last
u + k + q,quick
u + k + l + y + q,quickly
u + m + h,much
u + m + n,number
u + m + t + s,must
u + m + c + h,much
u + c + s + h,such
u + c + t,cut
u + c + t + n,country
' + a + s,say
' + a + n,any
' + m + a + n,many
o + Dup,off
o + f,of
o + f + f,food
o + d,do
o + s,so
o + y + b,boy
o + n,on
o + n + s + Dup,soon
o + l,line
o + l + Dup,oil
o + l + d,old
o + l + y,only
o + l + n + y,only
o + t,to
o + t + b,both
o + t + Dup,too
o + t + s,stop
o + t + s + p,stop
o + t + n,not
o + t + n + f,often
o + t + n + d,don't
o + t + n + p,point
o + a + n + h,another
o + a + l,also
o + a + l + s,also
o + w,own
o + w + h,how
o + w + s + h,show
o + w + n,now
o + w + n + d,down
o + w + l + f,follow
o + w + t,two
o + g,go
o + g + Dup,good
o + g + n + s,song
o + g + l + n,long
o + g + t,got
o + g + a + l,along
o + g + a + l + n,along
o + k + b,book
o + k + n,know
o + k + l,look
o + k + t,took
o + k + t + Dup,took
o + k + w + n,know
o + v + a + b,above
o + v + k,move
o + m + s,some
o + m + t + s,most
o + m + a + l,almost
o + m + a + l + s,almost
o + m + a + t + l + s,almost
o + c + f,food
o + c + l + s + h,school
o + u,our
o + u + f,four
o + u + s + h,should
o + u + y,you
o + u + n + f,found
o + u + n + f + f,found
o + u + n + s,sound
o + u + n + s + d,sound
o + u + l + s + d + f,should
o + u + l + s + h,should
o + u + t,out
o + u + a + b,about
o + u + a + t + b,about
o + u + w + l + d,would
o + u + g + n + y,young
o + u + g + t + h,thought
o + u + m + t + n,mountain
o + u + m + t + n + Dup,mountain
o + u + c + l + d,could
o + ' + t + n + d,don't
o + o + l,oil
o + o + t + n,into
o + o + t + n + p,point
o + o + u + w + t + h,without
o + o + u + m + a + t + n,mountain
i + f,if
i + f + f,different
i + d,did
i + RH_Thumb_1_Center,different
i + s,is
i + s + d,side
i + s + h,his
i + n,in
i + n + f + f,find
i + l,list
i + l + n,line
i + t,it
i + t + s + Dup,still
i + t + s + h,this
i + t + n,into
i + t + l + s,list
i + t + l + s + Dup,still
i + a + s,said
i + a + s + d,said
i + a + n,animal
i + w + h,which
i + w + h + Dup,which
i + w + l,will
i + w + l + Dup,will
i + w + l + h,while
i + w + t + h,with
i + g,give
i + g + b,big
i + g + h,high
i + g + n + h,night
i + g + l + h,light
i + g + t + n + h,thing
i + g + t + l + h,light
i + g + a + n,again
i + g + a + n + Dup,again
i + k + n + d,kind
i + k + l,like
i + k + t + n + h,think
i + v + l,live
i + m + h,him
i + m + p,important
i + m + s,miss
i + m + s + Dup,miss
i + m + l,mile
i + m + t,time
i + m + t + h,might
i + m + a + n,animal
i + m + a + n + Dup,animal
i + m + a + l + n,another
i + m + g + t + h,might
i + c + p,picture
i + c + t + y,city
i + u + t + l + n,until
i + u + w + t + h,without
i + u + k + q,quick
i + u + k + l + y + q,quickly
i + u + c + k + q,quick
i + u + c + k + l + y + q,quickly
i + ' + t,it's
i + ' + t + s,it's
e + b,be
e + Dup,earth
e + x,example
e + f + b,before
e + h,he
e + h + Dup,here
e + s,state
e + s + Dup,see
e + s + h,she
e + y + Dup,eye
e + n,name
e + n + b,been
e + n + Dup,need
e + n + d,end
e + l + h,help
e + l + h + f,help
e + l + s + p,spell
e + t,the
e + t + Dup,eat
e + t + f,feet
e + t + h,there
e + t + s,set
e + t + s + h,these
e + t + y + h,they
e + t + n + x,next
e + t + n + h,then
e + t + l,let
e + t + l + Dup,tell
e + t + l + f,left
e + a,at
e + a + f,father
e + a + d + f,head
e + a + h,hear
e + a + p,paper
e + a + s,sea
e + a + y,year
e + a + n,name
e + a + t,eat
e + a + t + s + Dup,state
e + w,we
e + w + f,few
e + w + n,new
e + w + n + h,when
e + w + l,well
e + w + l + Dup,well
e + w + t + b,between
e + w + t + n,went
e + w + t + n + b,between
e + g + h,here
e + g + t,get
e + g + a + p,page
e + g + a + n + b,began
e + k,keep
e + k + p,keep
e + k + t,take
e + k + a + t,take
e + v + y,every
e + v + n,even
e + v + a + h,have
e + v + a + l,leave
e + v + a + l + Dup,leave
e + m,me
e + m + s,seem
e + m + s + Dup,seem
e + m + n,men
e + m + t + h,them
e + m + a,make
e + m + a + d,made
e + m + a + s,same
e + m + a + n,mean
e + m + a + l + p + x,example
e + m + c + a,came
e + c + s,second
e + c + t + n + s,sentence
e + c + a,came
e + c + a + f,face
e + c + a + h,each
e + c + a + l + p,place
e + LH_Thumb_1_Center + a,make
e + u + s,use
e + u + s + q,question
e + u + n + d,under
e + u + t + q,quite
e + u + c + a + s + b,because
e + o + p,open
e + o + s + d,does
e + o + n,one
e + o + n + p,open
e + o + l + b,below
e + o + l + h,hello
e + o + l + h + Dup,hello
e + o + l + p,people
e + o + t + f,often
e + o + t + h,other
e + o + t + s + h,those
e + o + t + n + f,often
e + o + w + l + b,below
e + o + g + t + h,together
e + o + v,over
e + o + v + a + b,above
e + o + v + k,move
e + o + m,move
e + o + m + h,home
e + o + m + s,some
e + o + m + t + s,sometime
e + o + m + t + s + h,something
e + o + m + g + t + n + s + h,something
e + o + m + c,come
e + o + c,come
e + o + c + n,once
e + o + c + n + s + d,second
e + o + c + l + s,close
e + o + u + s + h,house
e + o + u + n,enough
e + o + u + g + n + h,enough
e + i + s + d,side
e + i + l + f,life
e + i + l + n,line
e + i + t + q,quite
e + i + t + l,little
e + i + t + l + Dup,little
e + i + a + d,idea
e + i + w + l + h,while
e + i + w + t + h,white
e + i + g,give
e + i + g + p,give
e + i + g + n + b,begin
e + i + k + l,like
e + i + v + l,live
e + i + m + l,mile
e + i + m + t,time
e + i + u + t + q,quite
e + i + u + u + t + n + s + q,question
r + e + h,her
r + e + t + Dup,tree
r + e + t + h,there
r + e + t + h + Dup,three
r + e + t + l,letter
r + e + a,are
r + e + a + d,read
r + e + a + h,hear
r + e + a + p,paper
r + e + a + n,near
r + e + a + l + y,really
r + e + a + l + y + Dup,really
r + e + a + t + f,after
r + e + a + t + h,earth
r + e + a + t + RH_Thumb_1_Center,father
r + e + a + t + l,learn
r + e + w,were
r + e + w + Dup,were
r + e + w + h,where
r + e + w + a + n + s,answer
r + e + w + a + t,water
r + e + g + h,here
r + e + g + a + l,large
r + e + g + a + t,great
r + e + v + y,very
r + e + v + y + Dup,every
r + e + v + n,never
r + e + u + n + d,under
r + e + u + m + n + b,number
r + e + o + f + b,before
r + e + o + t + h,other
r + e + o + g + t + h,together
r + e + o + v,over
r + e + o + m,more
r + e + o + m + t + h,mother
r + e + i + t + h,their
r + e + i + t + n + f + f,different
r + e + i + v,river
r + e + i + c + l + d + f,children
r + e + i + u + c + t + p,picture
r + f,for
r + h,her
r + y,your
r + n,near
r + l,learn
r + t + Dup,tree
r + t + f,father
r + t + s + Dup,start
r + t + y,try
r + t + l,letter
r + t + l + Dup,letter
r + a,are
r + a + f,far
r + a + d,read
r + a + d + f,hard
r + a + h,hard
r + a + p,part
r + a + l + y,really
r + a + l + y + Dup,really
r + a + t + p,part
r + a + t + s + Dup,start
r + w,were
r + w + h,where
r + w + t,water
r + w + a + n + s,answer
r + g + t,great
r + g + a + l,large
r + v + y,very
r + v + n,never
r + v + l,later
r + m + f,form
r + c + a,car
r + c + a + y,carry
r + c + a + y + Dup,carry
r + u + n,run
r + u + t + h,through
r + u + t + n,turn
r + ' + t + s,story
r + o,or
r + o + f,for
r + o + t + y + s,story
r + o + a + n + h,another
r + o + w,work
r + o + w + d,word
r + o + w + l + d,world
r + o + g,grow
r + o + LeftDoubleClick,grow
r + o + k + w,work
r + o + m,more
r + o + m + f,from
r + o + m + t + h,mother
r + o + u,our
r + o + u + f,four
r + o + u + y,your
r + o + u + a,around
r + o + u + a + n,around
r + o + u + a + n + d,around
r + o + u + g,group
r + o + u + g + p,group
r + o + u + g + t + h,through
r + o + u + c + t + n,country
r + o + u + c + t + n + y,country
r + i + t + h,their
r + i + t + s + f,first
r + i + a,air
r + i + w + t,write
r + i + g + h,right
r + i + g + l,girl
r + i + g + t + h,right
r + i + v,river
r + i + v + Dup,river
r + i + m + t + n + p,important
r + i + c + l + h,children
1 s + Dup say
2 y + b by
3 y + Dup why
4 n + Dup no
5 n + f find
6 l + f life
7 l + p people
8 l + s + p spell
9 l + n line
10 l + n + p plant
11 t + h that
12 t + n + h than
13 t + n + p plant
14 t + l + f left
15 a + f after
16 a + d add
17 a + d + f had
18 a + h has
19 a + s as
20 a + s + h has
21 a + y + d day
22 a + y + s say
23 a + n an
24 a + n + d and
25 a + n + d + f hand
26 a + n + h hand
27 a + n + y any
28 a + l all
29 a + l + d land
30 a + l + p plant
31 a + l + s last
32 a + l + y + p play
33 a + l + n + d land
34 a + t at
35 a + t + h that
36 a + t + n + h than
37 a + t + l + s last
38 a + t + l + n + p plant
39 - + ? question
40 w + h who
41 w + s saw
42 w + y + h why
43 w + t without
44 w + t + h watch
45 w + t + n went
46 w + a was
47 w + a + h what
48 w + a + s saw
49 w + a + y way
50 w + a + y + Dup away
51 w + a + n want
52 w + a + l + f walk
53 w + a + l + y always
54 w + a + l + y + s always
55 w + a + t watch
56 w + a + t + h watch
57 w + a + t + n want
58 g + b begin
59 g + Dup question
60 g + h here
61 g + p page
62 g + l + n long
63 g + t get
64 k + q quick
65 k + f and
66 k + l + y + q quickly
67 k + t + l talk
68 k + a + b back
69 k + a + s ask
70 k + a + t + l talk
71 k + w + a + l walk
72 m + f form
73 m + y my
74 m + t + Dup mountain
75 m + a + f family
76 m + a + s small
77 m + a + y may
78 m + a + n man
79 m + a + n + y many
80 m + a + l almost
81 m + a + l + s small
82 m + a + l + s + Dup small
83 c + b because
84 c + Dup sea
85 c + h head
86 c + a + n can
87 c + a + n + h change
88 c + a + l call
89 c + a + l + p place
90 c + k + a + b back
91 u + p us
92 u + y you
93 u + j just
94 u + j + s just
95 u + t + b but
96 u + t + p put
97 u + t + s + d study
98 u + t + n until
99 u + t + j + s just
100 u + a + l last
101 u + k + q quick
102 u + k + l + y + q quickly
103 u + m + h much
104 u + m + n number
105 u + m + t + s must
106 u + m + c + h much
107 u + c + s + h such
108 u + c + t cut
109 u + c + t + n country
110 ' + a + s say
111 ' + a + n any
112 ' + m + a + n many
113 o + Dup off
114 o + f of
115 o + f + f food
116 o + d do
117 o + s so
118 o + y + b boy
119 o + n on
120 o + n + s + Dup soon
121 o + l line
122 o + l + Dup oil
123 o + l + d old
124 o + l + y only
125 o + l + n + y only
126 o + t to
127 o + t + b both
128 o + t + Dup too
129 o + t + s stop
130 o + t + s + p stop
131 o + t + n not
132 o + t + n + f often
133 o + t + n + d don't
134 o + t + n + p point
135 o + a + n + h another
136 o + a + l also
137 o + a + l + s also
138 o + w own
139 o + w + h how
140 o + w + s + h show
141 o + w + n now
142 o + w + n + d down
143 o + w + l + f follow
144 o + w + t two
145 o + g go
146 o + g + Dup good
147 o + g + n + s song
148 o + g + l + n long
149 o + g + t got
150 o + g + a + l along
151 o + g + a + l + n along
152 o + k + b book
153 o + k + n know
154 o + k + l look
155 o + k + t took
156 o + k + t + Dup took
157 o + k + w + n know
158 o + v + a + b above
159 o + v + k move
160 o + m + s some
161 o + m + t + s most
162 o + m + a + l almost
163 o + m + a + l + s almost
164 o + m + a + t + l + s almost
165 o + c + f food
166 o + c + l + s + h school
167 o + u our
168 o + u + f four
169 o + u + s + h should
170 o + u + y you
171 o + u + n + f found
172 o + u + n + f + f found
173 o + u + n + s sound
174 o + u + n + s + d sound
175 o + u + l + s + d + f should
176 o + u + l + s + h should
177 o + u + t out
178 o + u + a + b about
179 o + u + a + t + b about
180 o + u + w + l + d would
181 o + u + g + n + y young
182 o + u + g + t + h thought
183 o + u + m + t + n mountain
184 o + u + m + t + n + Dup mountain
185 o + u + c + l + d could
186 o + ' + t + n + d don't
187 o + o + l oil
188 o + o + t + n into
189 o + o + t + n + p point
190 o + o + u + w + t + h without
191 o + o + u + m + a + t + n mountain
192 i + f if
193 i + f + f different
194 i + d did
195 i + RH_Thumb_1_Center different
196 i + s is
197 i + s + d side
198 i + s + h his
199 i + n in
200 i + n + f + f find
201 i + l list
202 i + l + n line
203 i + t it
204 i + t + s + Dup still
205 i + t + s + h this
206 i + t + n into
207 i + t + l + s list
208 i + t + l + s + Dup still
209 i + a + s said
210 i + a + s + d said
211 i + a + n animal
212 i + w + h which
213 i + w + h + Dup which
214 i + w + l will
215 i + w + l + Dup will
216 i + w + l + h while
217 i + w + t + h with
218 i + g give
219 i + g + b big
220 i + g + h high
221 i + g + n + h night
222 i + g + l + h light
223 i + g + t + n + h thing
224 i + g + t + l + h light
225 i + g + a + n again
226 i + g + a + n + Dup again
227 i + k + n + d kind
228 i + k + l like
229 i + k + t + n + h think
230 i + v + l live
231 i + m + h him
232 i + m + p important
233 i + m + s miss
234 i + m + s + Dup miss
235 i + m + l mile
236 i + m + t time
237 i + m + t + h might
238 i + m + a + n animal
239 i + m + a + n + Dup animal
240 i + m + a + l + n another
241 i + m + g + t + h might
242 i + c + p picture
243 i + c + t + y city
244 i + u + t + l + n until
245 i + u + w + t + h without
246 i + u + k + q quick
247 i + u + k + l + y + q quickly
248 i + u + c + k + q quick
249 i + u + c + k + l + y + q quickly
250 i + ' + t it's
251 i + ' + t + s it's
252 e + b be
253 e + Dup earth
254 e + x example
255 e + f + b before
256 e + h he
257 e + h + Dup here
258 e + s state
259 e + s + Dup see
260 e + s + h she
261 e + y + Dup eye
262 e + n name
263 e + n + b been
264 e + n + Dup need
265 e + n + d end
266 e + l + h help
267 e + l + h + f help
268 e + l + s + p spell
269 e + t the
270 e + t + Dup eat
271 e + t + f feet
272 e + t + h there
273 e + t + s set
274 e + t + s + h these
275 e + t + y + h they
276 e + t + n + x next
277 e + t + n + h then
278 e + t + l let
279 e + t + l + Dup tell
280 e + t + l + f left
281 e + a at
282 e + a + f father
283 e + a + d + f head
284 e + a + h hear
285 e + a + p paper
286 e + a + s sea
287 e + a + y year
288 e + a + n name
289 e + a + t eat
290 e + a + t + s + Dup state
291 e + w we
292 e + w + f few
293 e + w + n new
294 e + w + n + h when
295 e + w + l well
296 e + w + l + Dup well
297 e + w + t + b between
298 e + w + t + n went
299 e + w + t + n + b between
300 e + g + h here
301 e + g + t get
302 e + g + a + p page
303 e + g + a + n + b began
304 e + k keep
305 e + k + p keep
306 e + k + t take
307 e + k + a + t take
308 e + v + y every
309 e + v + n even
310 e + v + a + h have
311 e + v + a + l leave
312 e + v + a + l + Dup leave
313 e + m me
314 e + m + s seem
315 e + m + s + Dup seem
316 e + m + n men
317 e + m + t + h them
318 e + m + a make
319 e + m + a + d made
320 e + m + a + s same
321 e + m + a + n mean
322 e + m + a + l + p + x example
323 e + m + c + a came
324 e + c + s second
325 e + c + t + n + s sentence
326 e + c + a came
327 e + c + a + f face
328 e + c + a + h each
329 e + c + a + l + p place
330 e + LH_Thumb_1_Center + a make
331 e + u + s use
332 e + u + s + q question
333 e + u + n + d under
334 e + u + t + q quite
335 e + u + c + a + s + b because
336 e + o + p open
337 e + o + s + d does
338 e + o + n one
339 e + o + n + p open
340 e + o + l + b below
341 e + o + l + h hello
342 e + o + l + h + Dup hello
343 e + o + l + p people
344 e + o + t + f often
345 e + o + t + h other
346 e + o + t + s + h those
347 e + o + t + n + f often
348 e + o + w + l + b below
349 e + o + g + t + h together
350 e + o + v over
351 e + o + v + a + b above
352 e + o + v + k move
353 e + o + m move
354 e + o + m + h home
355 e + o + m + s some
356 e + o + m + t + s sometime
357 e + o + m + t + s + h something
358 e + o + m + g + t + n + s + h something
359 e + o + m + c come
360 e + o + c come
361 e + o + c + n once
362 e + o + c + n + s + d second
363 e + o + c + l + s close
364 e + o + u + s + h house
365 e + o + u + n enough
366 e + o + u + g + n + h enough
367 e + i + s + d side
368 e + i + l + f life
369 e + i + l + n line
370 e + i + t + q quite
371 e + i + t + l little
372 e + i + t + l + Dup little
373 e + i + a + d idea
374 e + i + w + l + h while
375 e + i + w + t + h white
376 e + i + g give
377 e + i + g + p give
378 e + i + g + n + b begin
379 e + i + k + l like
380 e + i + v + l live
381 e + i + m + l mile
382 e + i + m + t time
383 e + i + u + t + q quite
384 e + i + u + u + t + n + s + q question
385 r + e + h her
386 r + e + t + Dup tree
387 r + e + t + h there
388 r + e + t + h + Dup three
389 r + e + t + l letter
390 r + e + a are
391 r + e + a + d read
392 r + e + a + h hear
393 r + e + a + p paper
394 r + e + a + n near
395 r + e + a + l + y really
396 r + e + a + l + y + Dup really
397 r + e + a + t + f after
398 r + e + a + t + h earth
399 r + e + a + t + RH_Thumb_1_Center father
400 r + e + a + t + l learn
401 r + e + w were
402 r + e + w + Dup were
403 r + e + w + h where
404 r + e + w + a + n + s answer
405 r + e + w + a + t water
406 r + e + g + h here
407 r + e + g + a + l large
408 r + e + g + a + t great
409 r + e + v + y very
410 r + e + v + y + Dup every
411 r + e + v + n never
412 r + e + u + n + d under
413 r + e + u + m + n + b number
414 r + e + o + f + b before
415 r + e + o + t + h other
416 r + e + o + g + t + h together
417 r + e + o + v over
418 r + e + o + m more
419 r + e + o + m + t + h mother
420 r + e + i + t + h their
421 r + e + i + t + n + f + f different
422 r + e + i + v river
423 r + e + i + c + l + d + f children
424 r + e + i + u + c + t + p picture
425 r + f for
426 r + h her
427 r + y your
428 r + n near
429 r + l learn
430 r + t + Dup tree
431 r + t + f father
432 r + t + s + Dup start
433 r + t + y try
434 r + t + l letter
435 r + t + l + Dup letter
436 r + a are
437 r + a + f far
438 r + a + d read
439 r + a + d + f hard
440 r + a + h hard
441 r + a + p part
442 r + a + l + y really
443 r + a + l + y + Dup really
444 r + a + t + p part
445 r + a + t + s + Dup start
446 r + w were
447 r + w + h where
448 r + w + t water
449 r + w + a + n + s answer
450 r + g + t great
451 r + g + a + l large
452 r + v + y very
453 r + v + n never
454 r + v + l later
455 r + m + f form
456 r + c + a car
457 r + c + a + y carry
458 r + c + a + y + Dup carry
459 r + u + n run
460 r + u + t + h through
461 r + u + t + n turn
462 r + ' + t + s story
463 r + o or
464 r + o + f for
465 r + o + t + y + s story
466 r + o + a + n + h another
467 r + o + w work
468 r + o + w + d word
469 r + o + w + l + d world
470 r + o + g grow
471 r + o + LeftDoubleClick grow
472 r + o + k + w work
473 r + o + m more
474 r + o + m + f from
475 r + o + m + t + h mother
476 r + o + u our
477 r + o + u + f four
478 r + o + u + y your
479 r + o + u + a around
480 r + o + u + a + n around
481 r + o + u + a + n + d around
482 r + o + u + g group
483 r + o + u + g + p group
484 r + o + u + g + t + h through
485 r + o + u + c + t + n country
486 r + o + u + c + t + n + y country
487 r + i + t + h their
488 r + i + t + s + f first
489 r + i + a air
490 r + i + w + t write
491 r + i + g + h right
492 r + i + g + l girl
493 r + i + g + t + h right
494 r + i + v river
495 r + i + v + Dup river
496 r + i + m + t + n + p important
497 r + i + c + l + h children

View File

@@ -1,67 +0,0 @@
name: Action Codes
description: 10-bit action codes 0x00-0x1F
actions:
0x00:
id: "0x00"
0x01:
id: "0x01"
0x02:
id: "0x02"
0x03:
id: "0x03"
0x04:
id: "0x04"
0x05:
id: "0x05"
0x06:
id: "0x06"
0x07:
id: "0x07"
0x08:
id: "0x08"
0x09:
id: "0x09"
0x0A:
id: "0x0A"
0x0B:
id: "0x0B"
0x0C:
id: "0x0C"
0x0D:
id: "0x0D"
0x0E:
id: "0x0E"
0x0F:
id: "0x0F"
0x10:
id: "0x10"
0x11:
id: "0x11"
0x12:
id: "0x12"
0x13:
id: "0x13"
0x14:
id: "0x14"
0x15:
id: "0x15"
0x16:
id: "0x16"
0x17:
id: "0x17"
0x18:
id: "0x18"
0x19:
id: "0x19"
0x1A:
id: "0x1A"
0x1B:
id: "0x1B"
0x1C:
id: "0x1C"
0x1D:
id: "0x1D"
0x1E:
id: "0x1E"
0x1F:
id: "0x1F"

View File

@@ -0,0 +1,144 @@
name: ASCII Macros
description: ASCII Characters that are macros for SHFT + key
actions:
33:
id: "!"
title: Exclamation Point
34:
id: '"'
title: Double Quote
35:
id: "#"
title: Hash Symbol
36:
id: "$"
title: Dollar Sign
37:
id: "%"
title: Percent
38:
id: "&"
title: Ampersand
40:
id: "("
title: Opening Parenthesis
41:
id: ")"
title: Closing Parenthesis
42:
id: "*"
title: Asterisk
43:
id: "+"
title: Plus
58:
id: ":"
title: Colon
60:
id: "<"
title: Less Than
62:
id: ">"
title: Greater Than
63:
id: "?"
title: Question Mark
64:
id: "@"
title: At Symbol
65:
id: "A"
title: Uppercase A
66:
id: "B"
title: Uppercase B
67:
id: "C"
title: Uppercase C
68:
id: "D"
title: Uppercase D
69:
id: "E"
title: Uppercase E
70:
id: "F"
title: Uppercase F
71:
id: "G"
title: Uppercase G
72:
id: "H"
title: Uppercase H
73:
id: "I"
title: Uppercase I
74:
id: "J"
title: Uppercase J
75:
id: "K"
title: Uppercase K
76:
id: "L"
title: Uppercase L
77:
id: "M"
title: Uppercase M
78:
id: "N"
title: Uppercase N
79:
id: "O"
title: Uppercase O
80:
id: "P"
title: Uppercase P
81:
id: "Q"
title: Uppercase Q
82:
id: "R"
title: Uppercase R
83:
id: "S"
title: Uppercase S
84:
id: "T"
title: Uppercase T
85:
id: "U"
title: Uppercase U
86:
id: "V"
title: Uppercase V
87:
id: "W"
title: Uppercase W
88:
id: "X"
title: Uppercase X
89:
id: "Y"
title: Uppercase Y
90:
id: "Z"
title: Uppercase Z
94:
id: "^"
title: Caret
95:
id: "_"
title: Underscore
123:
id: "{"
title: Left Curly Brace
124:
id: "|"
title: Pipe
125:
id: "}"
title: Right Curly Brace
126:
id: "~"
title: Tilde

View File

@@ -7,289 +7,195 @@ actions:
description: |
While SPACE is used for keymaps and chord, just a " " is used in chord outputs.
This action is unique in this way. Technically it is "printable", but it is not visible.
33:
id: "!"
title: Exclamation Point
34:
id: '"'
title: Double Quote
35:
id: "#"
title: Hash Symbol
36:
id: "$"
title: Dollar Sign
37:
id: "%"
title: Percent
38:
id: "&"
title: Ampersand
39:
id: "'"
keyCode: Quote
title: Single Quote
40:
id: "("
title: Opening Parenthesis
41:
id: ")"
title: Closing Parenthesis
42:
id: "*"
title: Asterisk
43:
id: "+"
title: Plus
44:
id: ","
keyCode: Comma
title: Comma
45:
id: "-"
keyCode: Minus
title: Minus
46:
id: "."
keyCode: Period
title: Period
47:
id: "/"
keyCode: Slash
title: Forward Slash
48:
id: "0"
keyCode: Digit0
title: Zero
49:
id: "1"
keyCode: Digit1
title: One
50:
id: "2"
keyCode: Digit2
title: Two
51:
id: "3"
keyCode: Digit3
title: Three
52:
id: "4"
keyCode: Digit4
title: Four
53:
id: "5"
keyCode: Digit5
title: Five
54:
id: "6"
keyCode: Digit6
title: Six
55:
id: "7"
keyCode: Digit7
title: Seven
56:
id: "8"
keyCode: Digit8
title: Eight
57:
id: "9"
keyCode: Digit9
title: Nine
58:
id: ":"
title: Colon
59:
id: ";"
keyCode: Semicolon
title: Semicolon
60:
id: "<"
title: Less Than
61:
id: "="
keyCode: Equal
title: Equals
62:
id: ">"
title: Greater Than
63:
id: "?"
title: Question Mark
64:
id: "@"
title: At Symbol
65:
id: "A"
title: Uppercase A
66:
id: "B"
title: Uppercase B
67:
id: "C"
title: Uppercase C
68:
id: "D"
title: Uppercase D
69:
id: "E"
title: Uppercase E
70:
id: "F"
title: Uppercase F
71:
id: "G"
title: Uppercase G
72:
id: "H"
title: Uppercase H
73:
id: "I"
title: Uppercase I
74:
id: "J"
title: Uppercase J
75:
id: "K"
title: Uppercase K
76:
id: "L"
title: Uppercase L
77:
id: "M"
title: Uppercase M
78:
id: "N"
title: Uppercase N
79:
id: "O"
title: Uppercase O
80:
id: "P"
title: Uppercase P
81:
id: "Q"
title: Uppercase Q
82:
id: "R"
title: Uppercase R
83:
id: "S"
title: Uppercase S
84:
id: "T"
title: Uppercase T
85:
id: "U"
title: Uppercase U
86:
id: "V"
title: Uppercase V
87:
id: "W"
title: Uppercase W
88:
id: "X"
title: Uppercase X
89:
id: "Y"
title: Uppercase Y
90:
id: "Z"
title: Uppercase Z
91:
id: "["
keyCode: BracketLeft
title: Left Bracket
92:
id: "\\"
keyCode: Backslash
title: Backslash
93:
id: "]"
keyCode: BracketRight
title: Right Bracket
94:
id: "^"
title: Caret
95:
id: "_"
title: Underscore
96:
id: "`"
keyCode: Backquote
title: Backtick
97:
id: "a"
keyCode: KeyA
title: Lowercase a
98:
id: "b"
keyCode: KeyB
title: Lowercase b
99:
id: "c"
keyCode: KeyC
title: Lowercase c
100:
id: "d"
keyCode: KeyD
title: Lowercase d
101:
id: "e"
keyCode: KeyE
title: Lowercase e
102:
id: "f"
keyCode: KeyF
title: Lowercase f
103:
id: "g"
keyCode: KeyG
title: Lowercase g
104:
id: "h"
keyCode: KeyH
title: Lowercase h
105:
id: "i"
keyCode: KeyI
title: Lowercase i
106:
id: "j"
keyCode: KeyJ
title: Lowercase j
107:
id: "k"
keyCode: KeyK
title: Lowercase k
108:
id: "l"
keyCode: KeyL
title: Lowercase l
109:
id: "m"
keyCode: KeyM
title: Lowercase m
110:
id: "n"
keyCode: KeyN
title: Lowercase n
111:
id: "o"
keyCode: KeyO
title: Lowercase o
112:
id: "p"
keyCode: KeyP
title: Lowercase p
113:
id: "q"
keyCode: KeyQ
title: Lowercase q
114:
id: "r"
keyCode: KeyR
title: Lowercase r
115:
id: "s"
keyCode: KeyS
title: Lowercase s
116:
id: "t"
keyCode: KeyT
title: Lowercase t
117:
id: "u"
keyCode: KeyU
title: Lowercase u
118:
id: "v"
keyCode: KeyV
title: Lowercase v
119:
id: "w"
KeyCode: KeyW
title: Lowercase w
120:
id: "x"
keyCode: KeyX
title: Lowercase x
121:
id: "y"
keyCode: KeyY
title: Lowercase y
122:
id: "z"
keyCode: KeyZ
title: Lowercase z
123:
id: "{"
title: Left Curly Brace
124:
id: "|"
title: Pipe
125:
id: "}"
title: Right Curly Brace
126:
id: "~"
title: Tilde
127:
id: "DEL"
keyCode: Delete
title: Delete
icon: delete_forever

View File

@@ -6,34 +6,73 @@ type: unassigned
actions:
600:
id: "LH_THUMB_3_3D"
title: Left Hand Thumb Top 3D Click
title: "Left Hand Thumb Bottom 3D Click"
icon: "adjust"
601:
id: "LH_THUMB_2_3D"
title: Left Hand Thumb Middle 3D Click
title: "Left Hand Thumb Middle 3D Click"
icon: "adjust"
602:
id: "LH_THUMB_1_3D"
title: Left Hand Thumb Bottom 3D Click
title: "Left Hand Thumb Top 3D Click"
icon: "adjust"
603:
id: "LH_INDEX_3D"
title: Left Hand Index Finger 3D Click
title: "Left Hand Index Finger 3D Click"
icon: "adjust"
604:
id: "LH_MID_1_3D"
title: Left Hand Middle Finger 3D Click
title: "Left Hand Middle Finger 3D Click"
icon: "adjust"
605:
id: "LH_RING_1_3D"
title: Left Hand Ring Finger 3D Click
title: "Left Hand Ring Finger 3D Click"
icon: "adjust"
606:
id: "LH_PINKY_3D"
title: Left Hand Pinky 3D Click,
# TODO...
# ["607", "CharaChorder One", "LH_MID_2_3D", "", ""],
# ["608", "CharaChorder One", "LH_RING_2_3D", "", ""],
# ["609", "CharaChorder One", "RH_THUMB_3_3D", "", ""],
# ["610", "CharaChorder One", "RH_THUMB_2_3D", "", ""],
# ["611", "CharaChorder One", "RH_THUMB_1_3D", "", ""],
# ["612", "CharaChorder One", "RH_INDEX_3D", "", ""],
# ["613", "CharaChorder One", "RH_MID_1_3D", "", ""],
# ["614", "CharaChorder One", "RH_RING_1_3D", "", ""],
# ["615", "CharaChorder One", "RH_PINKY_3D", "", ""],
# ["616", "CharaChorder One", "RH_MID_2_3D", "", ""],
# ["617", "CharaChorder One", "RH_RING_2_3D", "", ""]
title: "Left Hand Pinky 3D Click"
icon: "adjust"
607:
id: "LH_MID_2_3D"
title: "Left Hand Middle Finger 2 3D Click"
icon: "adjust"
608:
id: "LH_RING_2_3D"
title: "Left Hand Ring Finger 2 3D Click"
icon: "adjust"
609:
id: "RH_THUMB_3_3D"
title: "Right Hand Thumb Bottom 3D Click"
icon: "adjust"
610:
id: "RH_THUMB_2_3D"
title: "Right Hand Thumb Middle 3D Click"
icon: "adjust"
611:
id: "RH_THUMB_1_3D"
title: "Right Hand Thumb Top 3D Click"
icon: "adjust"
612:
id: "RH_INDEX_3D"
title: "Right Hand Index Finger 3D Click"
icon: "adjust"
613:
id: "RH_MID_1_3D"
title: "Right Hand Middle Finger 3D Click"
icon: "adjust"
614:
id: "RH_RING_1_3D"
title: "Right Hand Ring Finger 3D Click"
icon: "adjust"
615:
id: "RH_PINKY_3D"
title: "Right Hand Pinky 3D Click"
icon: "adjust"
616:
id: "RH_MID_2_3D"
title: "Right Hand Middle Finger 2 3D Click"
icon: "adjust"
617:
id: "RH_RING_2_3D"
title: "Right Hand Ring Finger 2 3D Click"
icon: "adjust"

View File

@@ -1,6 +1,9 @@
name: CharaChorder
description: CharaChorder specific actions
actions:
0:
id: "NO_ACTION"
display: "No Action"
528:
id: "RESTART"
title: Restart Device
@@ -26,7 +29,7 @@ actions:
536:
id: "DUP"
title: Repeat Last Note
icon: control_point_duplicate
icon: copy_all
description: |
In character entry, it repeats your last input.
In chorded entry, it is used for words with repeating letters.
@@ -58,6 +61,7 @@ actions:
544:
variantOf: 36
id: "SPACERIGHT"
display: " "
title: Right Spacebar (eg CC Lite)
icon: space_bar
variant: right
@@ -66,6 +70,9 @@ actions:
title: Primary Keymap
icon: counter_1
variant: left
description: |
Acts as a toggle if the same action is not assigned
to the target layer
549:
variantOf: 548
<<: *primary_keymap
@@ -76,6 +83,9 @@ actions:
title: Numeric Layer
icon: counter_2
variant: left
description: |
Acts as a toggle if the same action is not assigned
to the target layer
551:
variantOf: 550
<<: *secondary_keymap
@@ -86,8 +96,44 @@ actions:
title: Function Layer
icon: counter_3
variant: left
description: |
Acts as a toggle if the same action is not assigned
to the target layer
553:
variationOf: 552
<<: *tertiary_keymap
id: "KM_3_R"
variant: right
558:
id: HOLD_COMPOUND
title: Dynamic Library
icon: layers
description: |
Allows for the activation & creation of dynamic chord libraries.
When included as part of a chord output,
that chord's input becomes the seed for a dynamic chord library,
and that library is activated.
Any new chords created while a dynamic library is active are established one level above its seed.
559:
id: RELEASE_COMPOUND
title: Base Library
icon: layers_clear
description: |
Re-activates your base chord library,
and deactivates any currently active dynamic chord library.
576:
id: ACTION_DELAY_1000
icon: clock_loader_90
description: Wait for one second
577:
id: ACTION_DELAY_100
icon: clock_loader_60
description: Wait for 100 milliseconds
578:
id: ACTION_DELAY_10
icon: clock_loader_40
description: Wait for 10 milliseconds
579:
id: ACTION_DELAY_1
icon: clock_loader_10
description: Wait for one millisecond

View File

@@ -4,41 +4,52 @@ icon: keyboard
actions:
512: &left_ctrl
id: "LEFT_CTRL"
display: CTRL
title: Control Keyboard Modifier
keyCode: ControlLeft
variant: left
icon: keyboard_control_key
513: &left_shift
id: "LEFT_SHIFT"
title: Shift Keyboard Modifier
keyCode: ShiftLeft
variant: left
icon: shift
514: &left_alt
id: "LEFT_ALT"
display: ALT
title: Alt Keyboard Modifier
keyCode: AltLeft
variant: left
icon: keyboard_option_key
515: &left_gui
id: "LEFT_GUI"
title: GUI Keyboard Modifier
keyCode: MetaLeft
icon: apps
variant: left
icon: keyboard_command_key
516:
variationOf: 512
<<: *left_ctrl
id: "RIGHT_CTRL"
keyCode: ControlRight
variant: right
517:
variationOf: 513
<<: *left_shift
id: "RIGHT_SHIFT"
keyCode: ShiftRight
variant: right
518:
variationOf: 514
<<: *left_alt
id: "RIGHT_ALT"
keyCode: AltRight
variant: right
519:
variationOf: 515
<<: *left_gui
id: "RIGHT_GUI"
keyCode: MetaRight
variant: right
520:
id: "RELEASE_MOD"
title: Release all keyboard modifiers
@@ -51,3 +62,11 @@ actions:
id: "RELEASE_KEYS"
title: Release all keys, but not keyboard modifiers
icon: text_rotate_up
523:
id: "PRESS_NEXT"
title: "Press and do not release the next key/action"
icon: download
524:
id: "RELEASE_NEXT"
title: "Release the next key/action in the sequence"
icon: upload

View File

@@ -1,16 +1,19 @@
export interface KeymapCategory {
name: string
description: string
icon?: string
type?: "unassigned"
actions: Record<number, Partial<ActionInfo>>
name: string;
description: string;
icon?: string;
display?: string;
type?: "unassigned";
actions: Record<number, Partial<ActionInfo>>;
}
export interface ActionInfo {
id: string
title: string
icon: string
description: string
variant: "left" | "right"
variantOf: number
id: string;
title: string;
icon: string;
display: string;
description: string;
variant: "left" | "right";
variantOf: number;
keyCode: string;
}

View File

@@ -1,9 +1,11 @@
name: Raw Scancodes
description: Raw Keyboard Scancodes
name: Key codes
description: OS-Layout sensitive keycodes
actions:
256:
id: "KSC_00"
icon: block
title: No Key Pressed
description: Also commonly used at the end of a chord to remove auto-spaces
257:
id: "KSC_01"
title: Keyboard Error Roll Over
@@ -15,322 +17,421 @@ actions:
title: Keyboard Error Undefined
260:
id: "KEY_A"
keyCode: "KeyA"
title: Keyboard a and A (US English)
description: Non US English keyboard users may prefer these Raw Scancodes
261:
id: "KEY_B"
keyCode: "KeyB"
title: Keyboard b and B (US English)
262:
id: "KEY_C"
keyCode: "KeyC"
title: Keyboard c and C (US English)
263:
id: "KEY_D"
keyCode: "KeyD"
title: Keyboard d and D (US English)
264:
id: "KEY_E"
keyCode: "KeyE"
title: Keyboard e and E (US English)
265:
id: "KEY_F"
keyCode: "KeyF"
title: Keyboard f and F (US English)
266:
id: "KEY_G"
keyCode: "KeyG"
title: Keyboard g and G (US English)
267:
id: "KEY_H"
keyCode: "KeyH"
title: Keyboard h and H (US English)
268:
id: "KEY_I"
keyCode: "KeyI"
title: Keyboard i and I (US English)
269:
id: "KEY_J"
keyCode: "KeyJ"
title: Keyboard j and J (US English)
270:
id: "KEY_K"
keyCode: "KeyK"
title: Keyboard k and K (US English)
271:
id: "KEY_L"
keyCode: "KeyL"
title: Keyboard l and L (US English)
272:
id: "KEY_M"
keyCode: "KeyM"
title: Keyboard m and M (US English)
273:
id: "KEY_N"
keyCode: "KeyN"
title: Keyboard n and N (US English)
274:
id: "KEY_O"
keyCode: "KeyO"
title: Keyboard o and O (US English)
275:
id: "KEY_P"
keyCode: "KeyP"
title: Keyboard p and P (US English)
276:
id: "KEY_Q"
keyCode: "KeyQ"
title: Keyboard q and Q (US English)
277:
id: "KEY_R"
keyCode: "KeyR"
title: Keyboard r and R (US English)
278:
id: "KEY_S"
keyCode: "KeyS"
title: Keyboard s and S (US English)
279:
id: "KEY_T"
keyCode: "KeyT"
title: Keyboard t and T (US English)
280:
id: "KEY_U"
keyCode: "KeyU"
title: Keyboard u and U (US English)
281:
id: "KEY_V"
keyCode: "KeyV"
title: Keyboard v and V (US English)
282:
id: "KEY_W"
keyCode: "KeyW"
title: Keyboard w and W (US English)
283:
id: "KEY_X"
keyCode: "KeyX"
title: Keyboard x and X (US English)
284:
id: "KEY_Y"
keyCode: "KeyY"
title: Keyboard y and Y (US English)
285:
id: "KEY_Z"
keyCode: "KeyZ"
title: Keyboard z and Z (US English)
286:
id: "KEY_1"
keyCode: "Digit1"
title: Keyboard 1 and ! (US English)
287:
id: "KEY_2"
keyCode: "Digit2"
title: Keyboard 2 and @ (US English)
288:
id: "KEY_3"
keyCode: "Digit3"
title: Keyboard 3 and # (US English)
289:
id: "KEY_4"
keyCode: "Digit4"
title: Keyboard 4 and $ (US English)
290:
id: "KEY_5"
keyCode: "Digit5"
title: Keyboard 5 and % (US English)
291:
id: "KEY_6"
keyCode: "Digit6"
title: Keyboard 6 and ^ (US English)
292:
id: "KEY_7"
keyCode: "Digit7"
title: Keyboard 7 and & (US English)
293:
id: "KEY_8"
keyCode: "Digit8"
title: Keyboard 8 and * (US English)
294:
id: "KEY_9"
keyCode: "Digit9"
title: Keyboard 9 and ( (US English)
295:
id: "KEY_0"
keyCode: "Digit0"
title: Keyboard 0 and ) (US English)
296:
id: "ENTER"
keyCode: "Enter"
title: Keyboard Return (US English)
icon: keyboard_return
297:
id: "ESC"
keyCode: "Escape"
title: Keyboard Escape (US English)
298:
id: "BKSP"
keyCode: "Backspace"
title: Keyboard Backspace (US English)
icon: backspace
299:
id: "TAB"
keyCode: "Tab"
title: Keyboard Tab (US English)
icon: keyboard_tab
300:
id: "KSC_2C"
keyCode: "Space"
title: Keyboard Space (US English)
description: |
The ASCII space is preferred over this raw scancode for the space bar.
icon: space_bar
301:
id: "KSC_2D"
keyCode: "Minus"
title: Keyboard - and _ (US English)
302:
id: "KSC_2E"
keyCode: "Equal"
title: Keyboard = and + (US English)
303:
id: "KSC_2F"
keyCode: "BracketLeft"
title: Keyboard [ and { (US English)
304:
id: "KSC_30"
keyCode: "BracketRight"
title: Keyboard ] and } (US English)
305:
id: "KSC_31"
keyCode: "Backslash"
title: Keyboard \ and | (US English)
306:
id: "KSC_32"
# TODO: also backslash?
title: Keyboard Non-US \# and ~ (US English)
307:
id: "KSC_33"
keyCode: "Semicolon"
title: "Keyboard ; and : (US English)"
308:
id: "KSC_34"
keyCode: "Quote"
title: Keyboard ' and " (US English)
309:
id: "KSC_35"
keyCode: "Backquote"
title: Keyboard ` and ~ (US English)
310:
id: "KSC_36"
keyCode: "Comma"
title: Keyboard , and < (US English)
311:
id: "KSC_37"
keyCode: "Period"
title: Keyboard . and > (US English)
312:
id: "KSC_38"
keyCode: "Slash"
title: Keyboard / and ? (US English)
313:
id: "CAPSLOCK"
keyCode: "CapsLock"
title: Keyboard Caps Lock
icon: shift_lock
314:
id: "F1"
keyCode: "F1"
title: Keyboard F1
315:
id: "F2"
keyCode: "F2"
title: Keyboard F2
316:
id: "F3"
keyCode: "F3"
title: Keyboard F3
317:
id: "F4"
keyCode: "F4"
title: Keyboard F4
318:
id: "F5"
keyCode: "F5"
title: Keyboard F5
319:
id: "F6"
keyCode: "F6"
title: Keyboard F6
320:
id: "F7"
keyCode: "F7"
title: Keyboard F7
321:
id: "F8"
keyCode: "F8"
title: Keyboard F8
322:
id: "F9"
keyCode: "F9"
title: Keyboard F9
323:
id: "F10"
keyCode: "F10"
title: Keyboard F10
324:
id: "F11"
keyCode: "F11"
title: Keyboard F11
325:
id: "F12"
keyCode: "F12"
title: Keyboard F12
326:
id: "PRTSCN"
keyCode: "PrintScreen"
title: Keyboard Print Screen
icon: screenshot_monitor
327:
id: "SCRLK"
keyCode: "ScrollLock"
title: Keyboard Scroll Lock
328:
id: "PAUSE"
keyCode: "Pause"
title: Keyboard Pause
329:
id: "INSERT"
keyCode: "Insert"
title: Keyboard Insert
icon: insert_text
330:
id: "HOME"
keyCode: "Home"
title: Keyboard Home
icon: home
331:
id: "PGUP"
keyCode: "PageUp"
title: Keyboard Page Up
icon: move_up
332:
id: "DELETE"
keyCode: "Delete"
title: Keyboard Delete Forward
333:
id: "END"
keyCode: "End"
title: Keyboard End
334:
id: "PGDN"
keyCode: "PageDown"
title: Keyboard Page Down
icon: move_down
335:
id: "ARROW_RT"
keyCode: "ArrowRight"
title: Keyboard Right Arrow
icon: keyboard_arrow_right
336:
id: "ARROW_LF"
keyCode: "ArrowLeft"
title: Keyboard Left Arrow
icon: keyboard_arrow_left
337:
id: "ARROW_DN"
keyCode: "ArrowDown"
title: Keyboard Down Arrow
icon: keyboard_arrow_down
338:
id: "ARROW_UP"
keyCode: "ArrowUp"
title: Keyboard Up Arrow
icon: keyboard_arrow_up
339:
id: "NUMLOCK"
keyCode: "NumLock"
title: Keyboard Num Lock and Clear
340:
id: "KP_SLASH"
keyCode: "NumpadDivide"
title: Keypad /
341:
id: "KP_ASTER"
keyCode: "NumpadStar"
title: Keypad *
342:
id: "KP_MINUS"
keyCode: "NumpadSubtract"
title: Keypad -
343:
id: "KP_PLUS"
keyCode: "NumpadAdd"
title: Keypad +
344:
id: "KP_ENTER"
keyCode: "NumpadEnter"
title: Keypad Enter
345:
id: "KP_1"
keyCode: "Numpad1"
title: Keypad 1 and End
346:
id: "KP_2"
keyCode: "Numpad2"
title: Keypad 2 and Down Arrow
347:
id: "KP_3"
keyCode: "Numpad3"
title: Keypad 3 and Page Down
348:
id: "KP_4"
keyCode: "Numpad4"
title: Keypad 4 and Left Arrow
349:
id: "KP_5"
keyCode: "Numpad5"
title: Keypad 5
350:
id: "KP_6"
keyCode: "Numpad6"
title: Keypad 6 and Rigth Arrow
351:
id: "KP_7"
keyCode: "Numpad7"
title: Keypad 7 and Home
352:
id: "KP_8"
keyCode: "Numpad8"
title: Keypad 8 and Up Arrow
353:
id: "KP_9"
keyCode: "Numpad9"
title: Keypad 9 and Page Up
354:
id: "KP_0"
keyCode: "Numpad0"
title: Keypad 0 and Insert
355:
id: "KP_DOT"
keyCode: "NumpadDecimal"
title: Keypad . and Delete
356:
id: "KSC_64"
keyCode: "IntlBackslash"
title: Keyboard Non-US \ and | (US English)
357:
id: "COMPOSE"
icon: menu
title: Keyboard Application
description: Officially supported by Win, Unix, and Boot
358:
id: "POWER"
keyCode: "Power"
title: Keyboard Power
description: Only officially supported by Mac and Unix
359:
id: "KP_EQUAL"
keyCode: "NumpadEqual"
title: Keypad =
description: Only officially supported by Mac
360:
@@ -787,10 +888,12 @@ actions:
description: Not required to be supported by any OS
472:
id: "KSC_D8"
keyCode: "NumpadClear"
title: Keypad Clear
description: Not required to be supported by any OS
473:
id: "KSC_D9"
keyCode: "NumpadClearEntry"
title: Keypad Clear Entry
description: Not required to be supported by any OS
474:
@@ -841,84 +944,99 @@ actions:
title: Keyboard Right GUI
488:
id: "KSC_E8"
icon: play_pause
keyCode: "MediaPlayPause"
title: Media Play Pause
description: Not required to be supported by any OS. Possibly deprecated.
489:
id: "KSC_E9"
icon: stop
keyCode: "MediaStop"
title: Media Stop CD
description: Not required to be supported by any OS. Possibly deprecated.
490:
id: "KSC_EA"
icon: skip_previous
keyCode: "MediaTrackPrevious"
title: Media Previous Song
description: Not required to be supported by any OS. Possibly deprecated.
491:
id: "KSC_EB"
icon: skip_next
keyCode: "MediaTrackNext"
title: Media Next Song
description: Not required to be supported by any OS. Possibly deprecated.
492:
id: "KSC_EC"
icon: eject
keyCode: "Eject"
title: Media Eject CD
description: Not required to be supported by any OS. Possibly deprecated.
description: MacOS only
493:
id: "KSC_ED"
icon: volume_up
keyCode: "AudioVolumeUp"
title: Media Volume Up
description: Not required to be supported by any OS. Possibly deprecated.
494:
id: "KSC_EE"
icon: volume_down
keyCode: "AudioVolumeDown"
title: Media Volume Down
description: Not required to be supported by any OS. Possibly deprecated.
495:
id: "KSC_EF"
icon: volume_off
keyCode: "AudioVolumeMute"
title: Media Mute
description: Not required to be supported by any OS. Possibly deprecated.
496:
id: "KSC_F0"
title: Media www
description: Not required to be supported by any OS. Possibly deprecated.
icon: language
title: Media Browser
497:
id: "KSC_F1"
title: Media Back
description: Not required to be supported by any OS. Possibly deprecated.
keyCode: "BrowserBack"
title: Media Browser Back
498:
id: "KSC_F2"
title: Media Forward
description: Not required to be supported by any OS. Possibly deprecated.
keyCode: "BrowserForward"
title: Media Browser Forward
499:
id: "KSC_F3"
title: Media Stop
description: Not required to be supported by any OS. Possibly deprecated.
keyCode: "BrowserStop"
title: Media Browser Stop
description: Not supported on MacOS
500:
id: "KSC_F4"
title: Media Find
description: Not required to be supported by any OS. Possibly deprecated.
icon: search
keyCode: "BrowserSearch"
title: Media Browser Search
501:
id: "KSC_F5"
title: Media Scroll Up
description: Not required to be supported by any OS. Possibly deprecated.
icon: brightness_high
title: Media Brightness Up
502:
id: "KSC_F6"
title: Media Scroll Down
description: Not required to be supported by any OS. Possibly deprecated.
icon: brightness_low
title: Media Brightness Down
503:
id: "KSC_F7"
title: Media Edit
description: Not required to be supported by any OS. Possibly deprecated.
504:
id: "KSC_F8"
title: Media Sleep
icon: bedtime
keyCode: "Sleep"
title: Media System Sleep
description: Not required to be supported by any OS. Possibly deprecated.
505:
id: "KSC_F9"
title: Media Coffee
description: Not required to be supported by any OS. Possibly deprecated.
icon: routine
keyCode: "WakeUp"
title: Media System Wake
description: Not supported on Windows
506:
id: "KSC_FA"
title: Media Refresh
description: Not required to be supported by any OS. Possibly deprecated.
keyCode: "BrowserRefresh"
title: Media Browser Refresh
507:
id: "KSC_FB"
title: Media Calc
description: Not required to be supported by any OS. Possibly deprecated.
title: Media Calculator
description: Not supported on MacOS
508:
id: "KSC_FC"
description: Not required to be supported by any OS.
@@ -930,4 +1048,4 @@ actions:
description: Not required to be supported by any OS.
511:
id: "KSC_FF"
description: Not required to be supported by any OS.
description: Not required to be supported by any OS.

View File

@@ -0,0 +1,142 @@
name: 103-key
col:
- row:
- key: 41
- key: 58
offset: [1, 0]
- key: 59
- key: 60
- key: 61
- key: 62
offset: [0.5, 0]
- key: 63
- key: 64
- key: 65
- key: 66
offset: [0.5, 0]
- key: 67
- key: 68
- key: 69
- key: 70
offset: [0.25, 0]
- key: 71
- key: 72
- offset: [0, 0.25]
row:
- key: 53
- key: 30
- key: 31
- key: 32
- key: 33
- key: 34
- key: 35
- key: 36
- key: 37
- key: 38
- key: 39
- key: 45
- key: 46
- key: 42
size: [2, 1]
- key: 73
offset: [0.25, 0]
- key: 74
- key: 75
- key: 83
offset: [0.25, 0]
- key: 84
- key: 85
- key: 86
- row:
- key: 43
size: [1.5, 1]
- key: 20
- key: 26
- key: 8
- key: 21
- key: 23
- key: 28
- key: 24
- key: 12
- key: 18
- key: 19
- key: 47
- key: 48
- key: 40
size: [1.5, 1]
- key: 76
offset: [0.25, 0]
- key: 77
- key: 78
- key: 95
offset: [0.25, 0]
- key: 96
- key: 97
- key: 87
size: [1, 2]
- offset: [0, -1]
row:
- key: 57
size: [2, 1]
- key: 4
- key: 22
- key: 7
- key: 9
- key: 10
- key: 11
- key: 13
- key: 14
- key: 15
- key: 51
- key: 52
- key: 49
size: [2, 1]
- key: 92
offset: [3.5, 0]
- key: 93
- key: 94
- row:
- key: 225
size: [2.5, 1]
- key: 29
- key: 27
- key: 6
- key: 25
- key: 5
- key: 17
- key: 16
- key: 54
- key: 55
- key: 56
- key: 229
size: [2.5, 1]
- key: 82
offset: [1.25, 0]
- key: 89
offset: [1.25, 0]
- key: 90
- key: 91
- key: 88
size: [1, 2]
- offset: [0, -1]
row:
- key: 224
size: [1.5, 1]
- key: 227
- key: 226
size: [1.5, 1]
- key: 44
size: [7, 1]
- key: 230
size: [1.5, 1]
- key: 231
- key: 228
size: [1.5, 1]
- key: 80
offset: [0.25, 0]
- key: 81
- key: 79
- key: 98
offset: [0.25, 0]
size: [2, 1]
- key: 99

View File

@@ -0,0 +1,86 @@
name: Lite
col:
- row:
- key: 53
- key: 54
- key: 55
- key: 56
- key: 57
- key: 58
- key: 59
- key: 60
- key: 61
- key: 62
- key: 63
- key: 64
- key: 65
- key: 66
size: [2, 1]
- row:
- key: 39
size: [1.5, 1]
- key: 40
- key: 41
- key: 42
- key: 43
- key: 44
- key: 45
- key: 46
- key: 47
- key: 48
- key: 49
- key: 50
- key: 51
- key: 52
size: [1.5, 1]
- row:
- key: 26
size: [1.75, 1]
- key: 27
- key: 28
- key: 29
- key: 30
- key: 31
- key: 32
- key: 33
- key: 34
- key: 35
- key: 36
- key: 37
- key: 38
size: [2.25, 1]
- row:
- key: 12
size: [2, 1]
- key: 13
- key: 14
- key: 15
- key: 16
- key: 17
- key: 18
- key: 19
- key: 20
- key: 21
- key: 22
- key: 23
- key: 24
- key: 25
- row:
- key: 0
- key: 1
size: [1.25, 1]
- key: 2
size: [1.25, 1]
- key: 3
size: [2, 1]
- key: 4
- key: 5
- key: 6
size: [2, 1]
- key: 7
size: [1.25, 1]
- key: 8
size: [1.25, 1]
- key: 9
- key: 10
- key: 11

View File

@@ -0,0 +1,37 @@
name: M4G
col:
# Ring / Middle
- offset: [2, 0]
row:
- switch: { e: 26, n: 27, w: 28, s: 29 }
- switch: { e: 21, n: 22, w: 23, s: 24 }
- offset: [4, 0]
switch: { w: 66, n: 67, e: 68, s: 69 }
- switch: { w: 71, n: 72, e: 73, s: 74 }
- offset: [2, 0]
row:
- switch: { e: 41, n: 42, w: 43, s: 44 }
- switch: { e: 36, n: 37, w: 38, s: 39 }
- offset: [4, 0]
switch: { w: 81, n: 82, e: 83, s: 84 }
- switch: { w: 86, n: 87, e: 88, s: 89 }
# Pinkie / Index
- offset: [0, -3]
row:
- switch: { e: 31, n: 32, w: 33, s: 34 }
- offset: [4, 0]
switch: { e: 16, n: 17, w: 18, s: 19 }
- switch: { w: 61, n: 62, e: 63, s: 64 }
- offset: [4, 0]
switch: { w: 76, n: 77, e: 78, s: 79 }
# Thumbs
- row:
- offset: [5.5, 0.5]
switch: { e: 11, n: 12, w: 13, s: 14 }
- offset: [1, 0.5]
switch: { w: 56, n: 57, e: 58, s: 59 }
- row:
- offset: [4.5, -0.25]
switch: { e: 6, n: 7, w: 8, s: 9 }
- offset: [3, -0.25]
switch: { w: 51, n: 52, e: 53, s: 54 }

View File

@@ -0,0 +1,37 @@
name: M4G
col:
# Ring / Middle
- offset: [2, 0]
row:
- switch: { e: 26, n: 27, w: 28, s: 29 }
- switch: { e: 21, n: 22, w: 23, s: 24 }
- offset: [4, 0]
switch: { w: 66, n: 67, e: 68, s: 69 }
- switch: { w: 71, n: 72, e: 73, s: 74 }
- offset: [2, 0]
row:
- switch: { e: 41, n: 42, w: 43, s: 44 }
- switch: { e: 36, n: 37, w: 38, s: 39 }
- offset: [4, 0]
switch: { w: 81, n: 82, e: 83, s: 84 }
- switch: { w: 86, n: 87, e: 88, s: 89 }
# Pinkie / Index
- offset: [0, -3]
row:
- switch: { e: 31, n: 32, w: 33, s: 34 }
- offset: [4, 0]
switch: { e: 16, n: 17, w: 18, s: 19 }
- switch: { w: 61, n: 62, e: 63, s: 64 }
- offset: [4, 0]
switch: { w: 76, n: 77, e: 78, s: 79 }
# Thumbs
- row:
- offset: [5.5, 0.5]
switch: { e: 11, n: 12, w: 13, s: 14 }
- offset: [1, 0.5]
switch: { w: 56, n: 57, e: 58, s: 59 }
- row:
- offset: [4.5, -0.25]
switch: { e: 6, n: 7, w: 8, s: 9 }
- offset: [3, -0.25]
switch: { w: 51, n: 52, e: 53, s: 54 }

View File

@@ -0,0 +1,42 @@
name: CC1
col:
# Ring / Middle
- offset: [2, 0]
row:
- switch: { d: 25, e: 26, n: 27, w: 28, s: 29 }
- switch: { d: 20, e: 21, n: 22, w: 23, s: 24 }
- offset: [4, 0]
switch: { d: 65, w: 66, n: 67, e: 68, s: 69 }
- switch: { d: 70, w: 71, n: 72, e: 73, s: 74 }
- offset: [2, 0]
row:
- switch: { d: 40, e: 41, n: 42, w: 43, s: 44 }
- switch: { d: 35, e: 36, n: 37, w: 38, s: 39 }
- offset: [4, 0]
switch: { d: 80, w: 81, n: 82, e: 83, s: 84 }
- switch: { d: 85, w: 86, n: 87, e: 88, s: 89 }
# Pinkie / Index
- offset: [0, -3]
row:
- switch: { d: 30, e: 31, n: 32, w: 33, s: 34 }
- offset: [4, 0]
switch: { d: 15, e: 16, n: 17, w: 18, s: 19 }
- switch: { d: 60, w: 61, n: 62, e: 63, s: 64 }
- offset: [4, 0]
switch: { d: 75, w: 76, n: 77, e: 78, s: 79 }
# Thumbs
- row:
- offset: [5.5, 0.5]
switch: { d: 10, e: 11, n: 12, w: 13, s: 14 }
- offset: [1, 0.5]
switch: { d: 55, w: 56, n: 57, e: 58, s: 59 }
- row:
- offset: [4.5, -0.25]
switch: { d: 5, e: 6, n: 7, w: 8, s: 9 }
- offset: [3, -0.25]
switch: { d: 50, w: 51, n: 52, e: 53, s: 54 }
- row:
- offset: [3.5, -0.25]
switch: { d: 0, e: 1, n: 2, w: 3, s: 4 }
- offset: [5, -0.25]
switch: { d: 45, w: 46, n: 47, e: 48, s: 49 }

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<mask id="cross" maskUnits="userSpaceOnUse">
<rect x="0" y="0" width="32" height="32" fill="white" />
<path d="M0 0L32 32M0 32L32 0" stroke="black" stroke-width="3" />
</mask>
<circle cx="16" cy="16" r="11.5" fill="none" stroke="white" stroke-width="9" mask="url(#cross)" />
</svg>

Before

Width:  |  Height:  |  Size: 433 B

View File

@@ -0,0 +1,38 @@
[
"You can use DUP+i to create chords on the fly in any text box",
"This site is open source! Check out the full source code on GitHub in the bottom left",
"Two letter chords can be activated accidentally in chentry. Be cautious around them",
"More inputs in a chord increase the tolerance, making them easier to activate",
"The maximum number of outputs in a chord is 256",
"You can create backups of your device on the top right",
"For programming you should set your auto-delete timeout to about 200ms",
"Large parts of this site were written on a CC1",
"I use VIM btw...",
"I use NixOS btw...",
"You can hold shift on the undo button to undo all changes",
"GTM stands for Generative Text Menu and can be used to change your device's settings anywhere",
"Ambidextrous Throwover (aka Mirror Mode) is a mode designed for one-handed use",
"Chentry stands for character entry, or typing letter by letter on a chording enabled device",
"Chord modifiers are hard-coded (as of now) and can be used in the English language to add conjugations and more",
"You can use 'cursor warping' by adding arrow key actions to a chord, for example to chord '()' with the cursor in the middle of the brackets",
"An arpeggiate is a single key press that modifies the preceding chord. Modifiers can be arpeggiated",
"Some actions are marked as a 'macro', which means the output is generated by a key sequence rather than a pure key press. Be cautious with those, as they can affect other keys when held together!",
"Spurring is a chording only mode which is more advanced, but can greatly improve typing speed when mastered",
"The forced chord phenomenon is when typing a word character by character starts to feel unnatural",
"Don't be afraid to delete chords you keep getting wrong",
"Most people find it easier to start their chord library from scratch rather than learning someone else's",
"A common techinque to deal with conflicts is to add DUP or the same key mirrored on the other hand",
"A longer chord is not always more difficult",
"Riley Keen made headlines when his Monkeytype score of 500WPM using a CC1 got him banned off the site",
"A 3d press refers to pressing down into a 5-way switch",
"The serial communication protocol used by CCOS is documented on docs.charachorder.com",
"The 'CCOS is ready' message can be turned off in the settings",
"Most people using the CC1 don't change the a-z key layout, as further modification provides very little benefit",
"Using VIM on the default CC1 a-z layout is perfectly doable, it's just a matter of getting used to it",
"You can use Nexus to track words you might want to add to your chord library",
"The CC1 default layout was 80% science, 20% art",
"There is little to no reason to use hjkl in VIM on a CC1 since the arrows keys are so close already",
"The device manager automatically creates a backup for you when you reboot your device into the bootloader",
"You can use \"compound\", \"macro\", \"suffix\" and \"cursor warp\" in the chord search to find specific types of chords",
"You can search for chord inputs by using a leading \"+\", for example \"+a +DUP\" will show all chords with inputs that contain both a and DUP"
]

154
src/lib/assets/settings.yml Normal file
View File

@@ -0,0 +1,154 @@
- name: spurring
description: |
"Chording only" mode which tells your device to output chords on a press
rather than a press & release. It also enables you to jump from one
chord to another without releasing everything and can be activated in
GTM or by chording both mirror keys. It can provide significant speed
gains with chording, but also takes away the flexibility of character
entry.
items:
- id: 0x41
name: enable
range: [0, 1]
- id: 0x43
name: character counter timeout
range: [0, 240000]
step: 1000
scale: 0.001
unit: s
- name: arpeggiates
description: |
Allows chord modifiers to be hit after instead of with a chord,
and enables select keys to be placed before auto-spaces.
items:
- id: 0x51
name: enable
range: [0, 1]
- id: 0x54
name: timeout
range: [0, 2550]
step: 10
unit: ms
- name: keyboard
items:
- id: 0x11
name: enable
range: [0, 1]
- id: 0x12
name: character entry
range: [0, 1]
- id: 0x13
name: command option swap
range: [0, 1]
description: |
Swaps ⌥ and ⌘ to make transitioning between Mac and other systems easier.
- id: 0x14
name: poll rate
range: [0, 255]
unit: Hz
inverse: 1000
- id: 0x15
name: debounce press
range: [0, 255]
unit: ms
- id: 0x16
name: debounce release
range: [0, 255]
unit: ms
- id: 0x17
name: output delay
range: [0, 10200]
step: 40
unit: µs
- name: mouse
items:
- id: 0x21
name: enable
range: [0, 1]
- id: 0x22
name: slow speed
range: [0, 255]
unit: px
- id: 0x23
name: fast speed
range: [0, 255]
unit: px
- id: 0x24
name: caffeine
range: [0, 1]
description: |
Keeps computer alive by moving the mouse back and forth one pixel every 60s
- id: 0x25
name: scroll speed
range: [0, 255]
unit: pg
- id: 0x26
name: poll rate
range: [0, 255]
unit: Hz
inverse: 1000
- name: chording
items:
- id: 0x31
name: enable
range: [0, 1]
- id: 0x33
name: auto delete timeout
range: [0, 25500]
step: 100
- id: 0x34
name: press tolerance
description: |
Scales with the number of chord inputs.
range: [0, 255]
unit: ms
- id: 0x35
name: release tolerance
description: |
Scales with the number of chord inputs.
range: [0, 255]
unit: ms
- name: leds
items:
- id: 0x84
name: enable
range: [0, 1]
- id: 0x81
name: brightness
range: [0, 50]
- id: 0x82
name: base color code
enum:
white: 0
red: 1
orange: 2
yellow: 3
charteuse: 4
green: 5
spring green: 6
cyan: 7
azure: 8
blue: 9
violet: 10
magenta: 11
rose: 12
rainbow: 13
- id: 0x83
name: highlight
range: [0, 1]
- name: misc
items:
- id: 0x91
name: operating system
enum:
windows: 0
mac: 1
linux: 2
ios: 3
android: 4
- id: 0x92
name: GTM realtime feedback
range: [0, 1]
- id: 0x93
name: startup message
range: [0, 1]

197
src/lib/backup/backup.ts Normal file
View File

@@ -0,0 +1,197 @@
import type {
CharaBackupFile,
CharaChordFile,
CharaFile,
CharaLayoutFile,
CharaSettingsFile,
} from "$lib/share/chara-file.js";
import type { Change } from "$lib/undo-redo.js";
import {
changes,
ChangeType,
chords,
layout,
settings,
} from "$lib/undo-redo.js";
import { get } from "svelte/store";
import { serialPort } from "../serial/connection";
import { csvLayoutToJson, isCsvLayout } from "$lib/backup/compat/legacy-layout";
import { isCsvChords, csvChordsToJson } from "./compat/legacy-chords";
export function downloadFile<T extends CharaFile<string>>(contents: T) {
const downloadUrl = URL.createObjectURL(
new Blob([JSON.stringify(contents)], { type: "application/json" }),
);
const element = document.createElement("a");
element.setAttribute(
"download",
`${contents.type}-${
get(serialPort)?.device
}-${new Date().toISOString()}.json`,
);
element.href = downloadUrl;
element.setAttribute("target", "_blank");
element.click();
URL.revokeObjectURL(downloadUrl);
}
export function downloadBackup() {
downloadFile<CharaBackupFile>({
charaVersion: 1,
type: "backup",
history: [
[createChordBackup(), createLayoutBackup(), createSettingsBackup()],
],
});
}
export function createLayoutBackup(): CharaLayoutFile {
return {
charaVersion: 1,
type: "layout",
device: get(serialPort)?.device,
layout: get(layout).map((it) => it.map((it) => it.action)) as [
number[],
number[],
number[],
],
};
}
export function createChordBackup(): CharaChordFile {
return {
charaVersion: 1,
type: "chords",
chords: get(chords).map((it) => [it.actions, it.phrase]),
};
}
export function createSettingsBackup(): CharaSettingsFile {
return {
charaVersion: 1,
type: "settings",
settings: get(settings).map((it) => it.value),
};
}
export async function restoreBackup(event: Event) {
const input = (event.target as HTMLInputElement).files![0];
if (!input) return;
const text = await input.text();
if (input.name.endsWith(".json")) {
restoreFromFile(JSON.parse(text));
} else if (isCsvLayout(text)) {
restoreFromFile(csvLayoutToJson(text));
} else if (isCsvChords(text)) {
restoreFromFile(csvChordsToJson(text));
} else {
}
}
export function restoreFromFile(
file: CharaBackupFile | CharaSettingsFile | CharaLayoutFile | CharaChordFile,
) {
if (file.charaVersion !== 1) throw new Error("Incompatible backup");
switch (file.type) {
case "backup": {
const recent = file.history[0];
if (!recent) return;
let backupDevice = recent[1].device;
if (backupDevice === "TWO") backupDevice = "ONE";
let currentDevice = get(serialPort)?.device;
if (currentDevice === "TWO") currentDevice = "ONE";
if (backupDevice !== currentDevice) {
alert("Backup is incompatible with this device");
throw new Error("Backup is incompatible with this device");
}
changes.update((changes) => {
changes.push([
...getChangesFromChordFile(recent[0]),
...getChangesFromLayoutFile(recent[1]),
...getChangesFromSettingsFile(recent[2]),
]);
return changes;
});
break;
}
case "chords": {
changes.update((changes) => {
changes.push(getChangesFromChordFile(file));
return changes;
});
break;
}
case "layout": {
changes.update((changes) => {
changes.push(getChangesFromLayoutFile(file));
return changes;
});
break;
}
case "settings": {
changes.update((changes) => {
changes.push(getChangesFromSettingsFile(file));
return changes;
});
break;
}
default: {
throw new Error(
`Unknown backup type "${(file as CharaFile<string>).type}"`,
);
}
}
}
export function getChangesFromChordFile(file: CharaChordFile) {
const changes: Change[] = [];
const existingChords = new Set(
get(chords).map(({ phrase, actions }) => JSON.stringify([actions, phrase])),
);
for (const [input, output] of file.chords) {
if (existingChords.has(JSON.stringify([input, output]))) {
continue;
}
changes.push({
type: ChangeType.Chord,
actions: input,
phrase: output,
id: input,
});
}
return changes;
}
export function getChangesFromSettingsFile(file: CharaSettingsFile) {
const changes: Change[] = [];
for (const [id, value] of file.settings.entries()) {
const setting = get(settings)[id];
if (setting !== undefined && setting.value !== value) {
changes.push({
type: ChangeType.Setting,
id,
setting: value,
});
}
}
return changes;
}
export function getChangesFromLayoutFile(file: CharaLayoutFile) {
const changes: Change[] = [];
for (const [layer, keys] of file.layout.entries()) {
for (const [id, action] of keys.entries()) {
if (get(layout)[layer]?.[id]?.action !== action) {
changes.push({
type: ChangeType.Layout,
layer,
id,
action,
});
}
}
}
return changes;
}

View File

@@ -0,0 +1,26 @@
e + b + a,babe
e + c + b,because
f + e + c + a,face
h + e + c + a,each
i + d + ',I'd
i + g + b,big
i + g + e,give
k + b + a,back
k + e + a,take
l + e + a,late
l + e + d + a,lead
l + f + e,feel
l + g + e + a,large
l + h + e,help
l + i + a,Lia
l + i + f,fill
l + i + f + e,life
l + i + g + b + a,gitlab
l + k + i + e,like
m + e + a,make
m + i + ',I'm
n + c + a,can
n + d + a,and
n + e + b,been
n + e + b + a,enable
n + e + d,end
1 e + b + a babe
2 e + c + b because
3 f + e + c + a face
4 h + e + c + a each
5 i + d + ' I'd
6 i + g + b big
7 i + g + e give
8 k + b + a back
9 k + e + a take
10 l + e + a late
11 l + e + d + a lead
12 l + f + e feel
13 l + g + e + a large
14 l + h + e help
15 l + i + a Lia
16 l + i + f fill
17 l + i + f + e life
18 l + i + g + b + a gitlab
19 l + k + i + e like
20 m + e + a make
21 m + i + ' I'm
22 n + c + a can
23 n + d + a and
24 n + e + b been
25 n + e + b + a enable
26 n + e + d end

View File

@@ -0,0 +1,31 @@
import { KEYMAP_IDS } from "$lib/serial/keymap-codes";
import type { CharaChordFile } from "$lib/share/chara-file";
const SPECIAL_KEYS = new Map<string, string>([[" ", "SPACE"]]);
export function csvChordsToJson(csv: string): CharaChordFile {
return {
charaVersion: 1,
type: "chords",
chords: csv
.trim()
.split("\n")
.map((line) => {
const [input, output] = line.split(/,(?=[^,]*$)/, 2);
return [
input!
.split("+")
.map((it) => KEYMAP_IDS.get(it.trim())?.code ?? 0)
.sort((a, b) => a - b),
output!
.trim()
.split("")
.map((it) => KEYMAP_IDS.get(SPECIAL_KEYS.get(it) ?? it)?.code ?? 0),
];
}),
};
}
export function isCsvChords(csv: string): boolean {
return /^([^+]+( *\+ *[^+]+)* *, *[^+, ]+ *(\n|(?=$)))+$/.test(csv);
}

View File

@@ -0,0 +1,27 @@
{
"charaVersion": 1,
"type": "layout",
"device": "one",
"layout": [
[
309, 304, 312, 303, 306, 290, 282, 301, 266, 285, 289, 270, 281, 272, 262,
288, 277, 298, 307, 264, 287, 268, 332, 311, 274, 286, 308, 329, 310, 280,
358, 512, 515, 513, 514, 313, 319, 318, 321, 320, 326, 315, 314, 317, 316,
312, 330, 331, 333, 334, 291, 261, 283, 536, 276, 292, 265, 275, 267, 263,
293, 260, 296, 544, 279, 294, 271, 299, 269, 273, 295, 284, 297, 302, 278,
357, 516, 519, 517, 518, 327, 336, 338, 335, 337, 328, 325, 322, 323, 324
],
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
],
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
]
}

View File

@@ -0,0 +1,270 @@
A1,0,309
A1,1,304
A1,2,312
A1,3,303
A1,4,306
A1,5,290
A1,6,282
A1,7,301
A1,8,266
A1,9,285
A1,10,289
A1,11,270
A1,12,281
A1,13,272
A1,14,262
A1,15,288
A1,16,277
A1,17,298
A1,18,307
A1,19,264
A1,20,287
A1,21,268
A1,22,332
A1,23,311
A1,24,274
A1,25,286
A1,26,308
A1,27,329
A1,28,310
A1,29,280
A1,30,358
A1,31,512
A1,32,515
A1,33,513
A1,34,514
A1,35,313
A1,36,319
A1,37,318
A1,38,321
A1,39,320
A1,40,326
A1,41,315
A1,42,314
A1,43,317
A1,44,316
A1,45,312
A1,46,330
A1,47,331
A1,48,333
A1,49,334
A1,50,291
A1,51,261
A1,52,283
A1,53,536
A1,54,276
A1,55,292
A1,56,265
A1,57,275
A1,58,267
A1,59,263
A1,60,293
A1,61,260
A1,62,296
A1,63,544
A1,64,279
A1,65,294
A1,66,271
A1,67,299
A1,68,269
A1,69,273
A1,70,295
A1,71,284
A1,72,297
A1,73,302
A1,74,278
A1,75,357
A1,76,516
A1,77,519
A1,78,517
A1,79,518
A1,80,327
A1,81,336
A1,82,338
A1,83,335
A1,84,337
A1,85,328
A1,86,325
A1,87,322
A1,88,323
A1,89,324
A2,0,0
A2,1,0
A2,2,0
A2,3,0
A2,4,0
A2,5,0
A2,6,0
A2,7,0
A2,8,0
A2,9,0
A2,10,0
A2,11,0
A2,12,0
A2,13,0
A2,14,0
A2,15,0
A2,16,0
A2,17,0
A2,18,0
A2,19,0
A2,20,0
A2,21,0
A2,22,0
A2,23,0
A2,24,0
A2,25,0
A2,26,0
A2,27,0
A2,28,0
A2,29,0
A2,30,0
A2,31,0
A2,32,0
A2,33,0
A2,34,0
A2,35,0
A2,36,0
A2,37,0
A2,38,0
A2,39,0
A2,40,0
A2,41,0
A2,42,0
A2,43,0
A2,44,0
A2,45,0
A2,46,0
A2,47,0
A2,48,0
A2,49,0
A2,50,0
A2,51,0
A2,52,0
A2,53,0
A2,54,0
A2,55,0
A2,56,0
A2,57,0
A2,58,0
A2,59,0
A2,60,0
A2,61,0
A2,62,0
A2,63,0
A2,64,0
A2,65,0
A2,66,0
A2,67,0
A2,68,0
A2,69,0
A2,70,0
A2,71,0
A2,72,0
A2,73,0
A2,74,0
A2,75,0
A2,76,0
A2,77,0
A2,78,0
A2,79,0
A2,80,0
A2,81,0
A2,82,0
A2,83,0
A2,84,0
A2,85,0
A2,86,0
A2,87,0
A2,88,0
A2,89,0
A3,0,0
A3,1,0
A3,2,0
A3,3,0
A3,4,0
A3,5,0
A3,6,0
A3,7,0
A3,8,0
A3,9,0
A3,10,0
A3,11,0
A3,12,0
A3,13,0
A3,14,0
A3,15,0
A3,16,0
A3,17,0
A3,18,0
A3,19,0
A3,20,0
A3,21,0
A3,22,0
A3,23,0
A3,24,0
A3,25,0
A3,26,0
A3,27,0
A3,28,0
A3,29,0
A3,30,0
A3,31,0
A3,32,0
A3,33,0
A3,34,0
A3,35,0
A3,36,0
A3,37,0
A3,38,0
A3,39,0
A3,40,0
A3,41,0
A3,42,0
A3,43,0
A3,44,0
A3,45,0
A3,46,0
A3,47,0
A3,48,0
A3,49,0
A3,50,0
A3,51,0
A3,52,0
A3,53,0
A3,54,0
A3,55,0
A3,56,0
A3,57,0
A3,58,0
A3,59,0
A3,60,0
A3,61,0
A3,62,0
A3,63,0
A3,64,0
A3,65,0
A3,66,0
A3,67,0
A3,68,0
A3,69,0
A3,70,0
A3,71,0
A3,72,0
A3,73,0
A3,74,0
A3,75,0
A3,76,0
A3,77,0
A3,78,0
A3,79,0
A3,80,0
A3,81,0
A3,82,0
A3,83,0
A3,84,0
A3,85,0
A3,86,0
A3,87,0
A3,88,0
A3,89,0
1 A1 0 309
2 A1 1 304
3 A1 2 312
4 A1 3 303
5 A1 4 306
6 A1 5 290
7 A1 6 282
8 A1 7 301
9 A1 8 266
10 A1 9 285
11 A1 10 289
12 A1 11 270
13 A1 12 281
14 A1 13 272
15 A1 14 262
16 A1 15 288
17 A1 16 277
18 A1 17 298
19 A1 18 307
20 A1 19 264
21 A1 20 287
22 A1 21 268
23 A1 22 332
24 A1 23 311
25 A1 24 274
26 A1 25 286
27 A1 26 308
28 A1 27 329
29 A1 28 310
30 A1 29 280
31 A1 30 358
32 A1 31 512
33 A1 32 515
34 A1 33 513
35 A1 34 514
36 A1 35 313
37 A1 36 319
38 A1 37 318
39 A1 38 321
40 A1 39 320
41 A1 40 326
42 A1 41 315
43 A1 42 314
44 A1 43 317
45 A1 44 316
46 A1 45 312
47 A1 46 330
48 A1 47 331
49 A1 48 333
50 A1 49 334
51 A1 50 291
52 A1 51 261
53 A1 52 283
54 A1 53 536
55 A1 54 276
56 A1 55 292
57 A1 56 265
58 A1 57 275
59 A1 58 267
60 A1 59 263
61 A1 60 293
62 A1 61 260
63 A1 62 296
64 A1 63 544
65 A1 64 279
66 A1 65 294
67 A1 66 271
68 A1 67 299
69 A1 68 269
70 A1 69 273
71 A1 70 295
72 A1 71 284
73 A1 72 297
74 A1 73 302
75 A1 74 278
76 A1 75 357
77 A1 76 516
78 A1 77 519
79 A1 78 517
80 A1 79 518
81 A1 80 327
82 A1 81 336
83 A1 82 338
84 A1 83 335
85 A1 84 337
86 A1 85 328
87 A1 86 325
88 A1 87 322
89 A1 88 323
90 A1 89 324
91 A2 0 0
92 A2 1 0
93 A2 2 0
94 A2 3 0
95 A2 4 0
96 A2 5 0
97 A2 6 0
98 A2 7 0
99 A2 8 0
100 A2 9 0
101 A2 10 0
102 A2 11 0
103 A2 12 0
104 A2 13 0
105 A2 14 0
106 A2 15 0
107 A2 16 0
108 A2 17 0
109 A2 18 0
110 A2 19 0
111 A2 20 0
112 A2 21 0
113 A2 22 0
114 A2 23 0
115 A2 24 0
116 A2 25 0
117 A2 26 0
118 A2 27 0
119 A2 28 0
120 A2 29 0
121 A2 30 0
122 A2 31 0
123 A2 32 0
124 A2 33 0
125 A2 34 0
126 A2 35 0
127 A2 36 0
128 A2 37 0
129 A2 38 0
130 A2 39 0
131 A2 40 0
132 A2 41 0
133 A2 42 0
134 A2 43 0
135 A2 44 0
136 A2 45 0
137 A2 46 0
138 A2 47 0
139 A2 48 0
140 A2 49 0
141 A2 50 0
142 A2 51 0
143 A2 52 0
144 A2 53 0
145 A2 54 0
146 A2 55 0
147 A2 56 0
148 A2 57 0
149 A2 58 0
150 A2 59 0
151 A2 60 0
152 A2 61 0
153 A2 62 0
154 A2 63 0
155 A2 64 0
156 A2 65 0
157 A2 66 0
158 A2 67 0
159 A2 68 0
160 A2 69 0
161 A2 70 0
162 A2 71 0
163 A2 72 0
164 A2 73 0
165 A2 74 0
166 A2 75 0
167 A2 76 0
168 A2 77 0
169 A2 78 0
170 A2 79 0
171 A2 80 0
172 A2 81 0
173 A2 82 0
174 A2 83 0
175 A2 84 0
176 A2 85 0
177 A2 86 0
178 A2 87 0
179 A2 88 0
180 A2 89 0
181 A3 0 0
182 A3 1 0
183 A3 2 0
184 A3 3 0
185 A3 4 0
186 A3 5 0
187 A3 6 0
188 A3 7 0
189 A3 8 0
190 A3 9 0
191 A3 10 0
192 A3 11 0
193 A3 12 0
194 A3 13 0
195 A3 14 0
196 A3 15 0
197 A3 16 0
198 A3 17 0
199 A3 18 0
200 A3 19 0
201 A3 20 0
202 A3 21 0
203 A3 22 0
204 A3 23 0
205 A3 24 0
206 A3 25 0
207 A3 26 0
208 A3 27 0
209 A3 28 0
210 A3 29 0
211 A3 30 0
212 A3 31 0
213 A3 32 0
214 A3 33 0
215 A3 34 0
216 A3 35 0
217 A3 36 0
218 A3 37 0
219 A3 38 0
220 A3 39 0
221 A3 40 0
222 A3 41 0
223 A3 42 0
224 A3 43 0
225 A3 44 0
226 A3 45 0
227 A3 46 0
228 A3 47 0
229 A3 48 0
230 A3 49 0
231 A3 50 0
232 A3 51 0
233 A3 52 0
234 A3 53 0
235 A3 54 0
236 A3 55 0
237 A3 56 0
238 A3 57 0
239 A3 58 0
240 A3 59 0
241 A3 60 0
242 A3 61 0
243 A3 62 0
244 A3 63 0
245 A3 64 0
246 A3 65 0
247 A3 66 0
248 A3 67 0
249 A3 68 0
250 A3 69 0
251 A3 70 0
252 A3 71 0
253 A3 72 0
254 A3 73 0
255 A3 74 0
256 A3 75 0
257 A3 76 0
258 A3 77 0
259 A3 78 0
260 A3 79 0
261 A3 80 0
262 A3 81 0
263 A3 82 0
264 A3 83 0
265 A3 84 0
266 A3 85 0
267 A3 86 0
268 A3 87 0
269 A3 88 0
270 A3 89 0

View File

@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import legacyLayout from "./legacy-layout.sample.csv?raw";
import legacyLayoutConverted from "./legacy-layout-converted.sample.json";
import { csvLayoutToJson, isCsvLayout } from "./legacy-layout";
describe("legacy layout", () => {
it("should detect a legacy layout", () => {
expect(isCsvLayout(legacyLayout)).to.be.true;
});
it("should not detect chord maps as layouts", () => {
expect(isCsvLayout("e + h + t,the")).to.be.false;
});
it("should convert legacy layouts", () => {
expect(csvLayoutToJson(legacyLayout)).to.deep.equal(legacyLayoutConverted);
});
});

View File

@@ -0,0 +1,28 @@
import type { CharaLayoutFile } from "$lib/share/chara-file";
/**
* Converts a legacy CSV-based layout to the modern JSON-based format
*/
export function csvLayoutToJson(
csv: string,
device: CharaLayoutFile["device"] = "one",
): CharaLayoutFile {
const layout: CharaLayoutFile = {
charaVersion: 1,
type: "layout",
device,
layout: [[], [], []],
};
for (const layer of csv.trim().split("\n")) {
const [layerId, key, action] = layer.substring(1).split(",").map(Number);
layout.layout[Number(layerId) - 1]![Number(key)] = Number(action);
}
return layout;
}
export function isCsvLayout(csv: string): boolean {
return /^(A[123],\d+,\d+\n?)+$/.test(csv);
}

View File

@@ -0,0 +1,140 @@
<script lang="ts">
import { browser } from "$app/environment";
import { ReplayPlayer } from "./core/player.js";
import { ReplayStepper } from "./core/step.js";
import type { Replay } from "./core/types.js";
import { TextRenderer } from "./renderer/renderer.js";
import { setContext, type Snippet } from "svelte";
let {
replay,
cursor = false,
keys = false,
children,
ondone,
}: {
replay: ReplayPlayer | Replay;
cursor?: boolean;
keys?: boolean;
children?: Snippet;
ondone?: () => void;
} = $props();
let replayPlayer: ReplayPlayer | undefined = $state();
setContext("replay", {
get player() {
return replayPlayer;
},
});
let finalText = $derived(
replay instanceof ReplayPlayer
? undefined
: new ReplayStepper(replay.keys).text.map((token) => token.text).join(""),
);
let svg: SVGSVGElement | undefined = $state();
let text: Text = (browser ? document.createTextNode("") : undefined)!;
let textRenderer: TextRenderer | undefined = $state();
$effect(() => {
if (!textRenderer) return;
textRenderer.showCursor = cursor;
});
$effect(() => {
if (!svg || !text) return;
const player =
replay instanceof ReplayPlayer ? replay : new ReplayPlayer(replay);
replayPlayer = player;
const renderer = new TextRenderer(svg.parentNode as HTMLElement, svg, text);
const apply = () => {
text.textContent =
finalText ??
(player.stepper.text.map((token) => token.text).join("") || "n");
renderer.text = player.stepper.text;
renderer.cursor = player.stepper.cursor;
if (keys) {
renderer.held = player.stepper.held;
}
};
const unsubscribePlayer = player.subscribe(apply);
textRenderer = renderer;
player.onDone = ondone;
player.start();
apply();
setTimeout(() => {
renderer.animated = true;
});
return () => {
unsubscribePlayer();
player?.destroy();
};
});
export function innerText(node: HTMLElement, text: Text) {
node.appendChild(text);
return {
destroy() {
text.remove();
},
};
}
</script>
{#key replay}
<svg bind:this={svg}></svg>
{#if browser}
<span use:innerText={text}></span>
{:else if !(replay instanceof ReplayPlayer)}
{finalText}
{/if}
{/key}
{#if children}
{@render children()}
{/if}
<style>
:global(*):has(svg) {
position: relative;
}
span {
opacity: 0;
white-space: pre-wrap;
overflow-wrap: break-word;
}
svg {
position: absolute;
top: 0;
left: 0;
font-family: inherit;
font-size: inherit;
color: inherit;
user-select: none;
}
svg > :global(text) {
font-family: inherit;
font-size: inherit;
fill: currentColor;
dominant-baseline: middle;
}
svg > :global(text[incorrect]) {
fill: red;
}
svg > :global(rect) {
fill: currentcolor;
}
svg > :global(.animated) {
transition: transform 100ms ease;
}
</style>

View File

@@ -0,0 +1,130 @@
<script lang="ts">
import { fly, scale } from "svelte/transition";
import { KBD_ICONS } from "./renderer/kbd-icon.js";
import { expoOut } from "svelte/easing";
import type { InferredChord } from "./core/types.js";
let { chords }: { chords: InferredChord[] } = $props();
function getPercent(
deviation: number,
inputCount: number,
perfect: number,
fail: number,
) {
const failAdjusted = fail * inputCount;
const perfectAdjusted = perfect * inputCount;
return Math.min(
1,
Math.max(
0,
Math.max(0, deviation - perfectAdjusted) /
(failAdjusted - perfectAdjusted),
),
);
}
function getColor(percent: number, alpha = 1) {
return `hsl(${(1 - percent) * 120}deg 50% 50% / ${alpha})`;
}
</script>
<section>
{#each chords as { input, id, deviation }, i (id)}
{@const a = getPercent(deviation[0], input.length, 10, 25)}
{@const b = getPercent(deviation[1], input.length, 10, 18)}
{@const max = Math.max(a, b)}
<div
class="chord"
out:fly={{ x: -100 }}
style:translate="calc(-{(chords.length - i - 1) * 5}em - 50%) 0"
style:scale={1 - (chords.length - i) / 6}
style:opacity={1 - (chords.length - i - 1) / 6}
title="Press: {deviation[0]}ms, Release: {deviation[1]}ms"
>
<div
class="rating"
style:color={getColor(max)}
style:text-shadow="0 0 {Math.round((1 - max) * 10)}px {getColor(
max,
0.6,
)}"
in:scale={{
start: 1.5 + 1.2 * (1 - max),
easing: expoOut,
duration: 1000,
}}
>
{#if max === 1}
Close
{:else if max > 0.5}
Okay
{:else if max > 0}
Good
{:else}
Perfect
{/if}
</div>
<div
in:fly={{ y: 20, easing: expoOut, duration: 1000 }}
class="tile"
style:background="linear-gradient(to right, {getColor(a)}, {getColor(
b,
)})"
></div>
<div in:fly={{ y: 60, easing: expoOut, duration: 1000 }}>
{#each input as token}
<kbd>{KBD_ICONS.get(token.code)}</kbd>
{/each}
</div>
</div>
{/each}
</section>
<style>
section {
position: relative;
margin: 1em;
margin-bottom: 0;
display: grid;
height: 3em;
font-size: 2em;
}
.rating {
font-weight: bold;
font-style: italic;
text-transform: uppercase;
}
.tile {
width: 100%;
height: 0.2em;
border-radius: 0.1em;
}
kbd {
font-size: 0.6em;
}
kbd + kbd {
margin-inline-start: 0.3em;
}
.chord {
will-change: transform, opacity, scale;
position: absolute;
top: 0;
left: 50%;
display: flex;
flex-direction: column;
margin-inline-end: 1em;
padding-inline: 0.1em;
justify-content: center;
align-items: center;
transition:
opacity 0.3s ease,
translate 0.3s ease,
scale 0.3s ease;
}
</style>

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import { getContext } from "svelte";
import { browser } from "$app/environment";
import type { InferredChord } from "./core/types.js";
import { ChordsReplayPlugin } from "./core/plugins/chords.js";
import type { ReplayPlayer } from "./core/player.js";
const player: { player: ReplayPlayer | undefined } = getContext("replay");
let {
chords = $bindable([]),
count = 1,
}: {
chords: InferredChord[];
count?: number;
} = $props();
if (browser) {
$effect(() => {
if (!player.player) return;
const tracker = new ChordsReplayPlugin();
tracker.register(player.player);
const unsubscribe = tracker.subscribe((value) => {
chords = value.slice(-count);
});
return unsubscribe;
});
}
</script>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { getContext } from "svelte";
import { RollingWpmReplayPlugin } from "./core/plugins/rolling-wpm";
import type { ReplayPlayer } from "./core/player";
const player: { player: ReplayPlayer | undefined } = getContext("replay");
let { wpm = $bindable(0) } = $props();
$effect(() => {
if (!player.player) return;
const tracker = new RollingWpmReplayPlugin();
tracker.register(player.player);
const unsubscribe = tracker.subscribe((value) => {
wpm = value;
});
return unsubscribe;
});
</script>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { getContext } from "svelte";
import type { ReplayPlayer } from "./core/player";
import { TextPlugin } from "./core/plugins/text";
const player: { player: ReplayPlayer | undefined } = getContext("replay");
let { text = $bindable("") } = $props();
$effect(() => {
if (!player.player) return;
const tracker = new TextPlugin();
tracker.register(player.player);
const unsubscribe = tracker.subscribe((value) => {
text = value;
});
return unsubscribe;
});
</script>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { getContext } from "svelte";
import { WpmReplayPlugin } from "./core/plugins/wpm";
import type { ReplayPlayer } from "./core/player";
const player: { player: ReplayPlayer | undefined } = getContext("replay");
let { wpm = $bindable(0) } = $props();
$effect(() => {
if (!player.player) return;
const tracker = new WpmReplayPlugin();
tracker.register(player.player);
const unsubscribe = tracker.subscribe((value) => {
wpm = value;
});
return unsubscribe;
});
</script>

View File

@@ -0,0 +1,146 @@
import { ReplayStepper } from "./step";
import type { ReplayPlugin, Replay, TextToken } from "./types";
export const ROBOT_THRESHOLD = 20;
export class ReplayPlayer {
stepper = new ReplayStepper();
private replayCursor = 0;
private releaseAt = new Map<string, number>();
startTime = performance.now();
private animationFrameId: number | null = null;
timescale = 1;
private subscribers = new Set<(value: TextToken | undefined) => void>();
onDone?: () => void;
constructor(
readonly replay: Replay,
plugins: ReplayPlugin[] = [],
) {
for (const plugin of plugins) {
plugin.register(this);
}
}
/** @type {import('./types').StoreContract<import('./types').TextToken | undefined>['subscribe']} */
subscribe(subscription: (value: TextToken | undefined) => void) {
this.subscribers.add(subscription);
return () => this.subscribers.delete(subscription);
}
private updateLoop() {
if (
this.replayCursor >= this.replay.keys.length &&
this.releaseAt.size === 0
) {
if (this.onDone) {
this.onDone();
}
return;
}
const now = performance.now() - this.startTime;
while (
this.replayCursor < this.replay.keys.length &&
this.replay.keys[this.replayCursor]![2] * this.timescale -
this.replay.start <=
now
) {
const [key, code, at, duration] = this.replay.keys[this.replayCursor++]!;
this.stepper.held.set(code, duration > ROBOT_THRESHOLD);
this.releaseAt.set(code, now + duration * this.timescale);
const token = this.stepper.step(key, code, at, duration);
for (const subscription of this.subscribers) {
subscription(token);
}
}
for (const [key, releaseAt] of this.releaseAt) {
if (releaseAt > now) continue;
this.stepper.held.delete(key);
this.releaseAt.delete(key);
for (const subscription of this.subscribers) {
subscription(undefined);
}
}
this.animationFrameId = requestAnimationFrame(this.updateLoop.bind(this));
}
playLiveEvent(key: string, code: string): (duration: number) => void {
this.replay.start = this.startTime;
const at = performance.now();
this.stepper.held.set(code, false);
const token = this.stepper.step(key, code, at) ?? {
text: key,
code,
stamp: at,
correct: true,
source: "robot",
};
for (const subscription of this.subscribers) {
subscription(token);
}
const timeout = setTimeout(() => {
token.source = "human";
this.stepper.held.set(code, true);
for (const subscription of this.subscribers) {
subscription(undefined);
}
}, ROBOT_THRESHOLD);
return (duration) => {
clearTimeout(timeout);
if (token) {
// TODO: will this cause performance issues with long text?
const index = this.stepper.text.indexOf(token);
if (index >= 0) {
this.stepper.text[index]!.duration = duration;
this.stepper.text[index]!.source =
duration < ROBOT_THRESHOLD ? "robot" : "human";
}
}
this.stepper.held.delete(code);
for (const subscription of this.subscribers) {
subscription(undefined);
}
};
}
start(delay = 200): this {
this.replayCursor = 0;
this.stepper = new ReplayStepper([], this.replay.challenge);
if (this.replay.keys.length === 0) {
if (this.onDone) {
this.onDone();
}
return this;
}
setTimeout(() => {
this.startTime = performance.now();
this.animationFrameId = requestAnimationFrame(this.updateLoop.bind(this));
}, delay);
return this;
}
destroy() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
}
}

View File

@@ -0,0 +1,111 @@
import { ReplayPlayer, ROBOT_THRESHOLD } from "../player";
import type {
StoreContract,
ReplayPlugin,
InferredChord,
TextToken,
} from "../types";
function isValid(human: TextToken[], robot: TextToken[]) {
return human.length > 1 && human.length <= 10 && robot.length > 0;
}
export class ChordsReplayPlugin
implements StoreContract<InferredChord[]>, ReplayPlugin
{
private readonly subscribers = new Set<(value: InferredChord[]) => void>();
private readonly chords: InferredChord[] = [];
private tokens: TextToken[] = [];
private timeout: Parameters<typeof clearTimeout>[0] = NaN;
private infer(human: TextToken[], robo: TextToken[]) {
const output = robo
.filter((token) => token.text.length === 1)
.map((token) => token.text)
.join("");
this.chords.push({
id: human.reduce((acc, curr) => Math.max(acc, curr.stamp), 0),
input: human,
output,
deviation: [
human.reduce((acc, curr) => Math.max(acc, curr.stamp), 0) -
human.reduce((acc, curr) => Math.min(acc, curr.stamp), Infinity),
human.reduce(
(acc, curr) => Math.max(acc, curr.stamp + (curr.duration ?? 0)),
0,
) -
human.reduce(
(acc, curr) => Math.min(acc, curr.stamp + (curr.duration ?? 0)),
Infinity,
),
],
});
for (const subscription of this.subscribers) {
subscription(this.chords);
}
}
register(replay: ReplayPlayer) {
replay.subscribe((token) => {
if (token) {
this.tokens.push(token);
}
let last = NaN;
let roboStart = NaN;
let roboEnd = NaN;
for (let i = 0; i < this.tokens.length; i++) {
const token = this.tokens[i]!;
if (!token.duration || !token.source) break;
if (
Number.isNaN(roboStart) &&
token.source === "human" &&
token.stamp > last
) {
this.tokens = [];
}
if (Number.isNaN(last) || token.stamp + token.duration > last) {
last = token.stamp + token.duration;
}
if (Number.isNaN(roboStart) && token.source === "robot") {
roboStart = i;
} else if (!Number.isNaN(roboStart) && token.source === "human") {
roboEnd = i;
const human = this.tokens.splice(0, roboStart);
const robot = this.tokens.splice(0, roboEnd - roboStart);
if (isValid(human, robot)) {
this.infer(human, robot);
}
}
}
clearTimeout(this.timeout);
if (replay.stepper.held.size === 0) {
this.timeout = setTimeout(() => {
if (this.tokens.length > 0) {
const human = this.tokens.splice(
0,
this.tokens.findIndex((it) => it.source === "robot"),
);
const robot = this.tokens.splice(0, this.tokens.length);
if (isValid(human, robot)) {
this.infer(human, robot);
}
}
}, ROBOT_THRESHOLD);
}
});
}
subscribe(subscription: (value: InferredChord[]) => void) {
this.subscribers.add(subscription);
return () => this.subscribers.delete(subscription);
}
}

View File

@@ -0,0 +1,71 @@
import { ReplayPlayer, ROBOT_THRESHOLD } from "../player";
import type { GraphData, ReplayPlugin, StoreContract } from "../types";
export class MetaReplayPlugin
implements StoreContract<GraphData>, ReplayPlugin
{
private subscribers = new Set<(value: GraphData) => void>();
private graphData: GraphData = { min: [0, 0], max: [0, 0], tokens: [] };
private liveHeldRoboFilter = new Set<string>();
register(replay: ReplayPlayer) {
replay.subscribe((token) => {
if (!token) return;
const lastHeld = this.graphData.tokens
.at(-1)
?.reduce(
(acc, curr) => Math.max(acc, curr.stamp + (curr.duration ?? 0)),
0,
);
if (
lastHeld &&
(lastHeld === -1 || lastHeld > token.stamp + (token.duration ?? 0))
) {
this.graphData.tokens.at(-1)!.push(token);
} else {
this.graphData.tokens.push([token]);
}
if (this.graphData.tokens.length === 1) {
this.graphData.min = [token.stamp, 0];
}
this.graphData.max = [
this.graphData.tokens
.at(-1)!
.reduce(
(acc, { stamp, duration }) =>
Math.max(acc, stamp + (duration ?? 0)),
0,
),
Math.max(this.graphData.max[1], this.graphData.tokens.at(-1)!.length),
];
this.liveHeldRoboFilter.add(token.code);
if (token.duration === undefined) {
setTimeout(() => {
if (this.liveHeldRoboFilter.has(token.code)) {
token.source = "human";
for (const subscription of this.subscribers) {
subscription(this.graphData);
}
}
}, ROBOT_THRESHOLD);
} else {
setTimeout(() => {
this.liveHeldRoboFilter.delete(token.code);
}, token.duration);
}
for (const subscription of this.subscribers) {
subscription(this.graphData);
}
});
}
subscribe(subscription: (value: GraphData) => void) {
this.subscribers.add(subscription);
return () => this.subscribers.delete(subscription);
}
}

View File

@@ -0,0 +1,48 @@
import type { ReplayPlayer } from "../player";
import type { ReplayPlugin, StoreContract } from "../types";
import { avgWordLength } from "./wpm";
export class RollingWpmReplayPlugin
implements StoreContract<number>, ReplayPlugin
{
subscribers = new Set<(value: number) => void>();
register(replay: ReplayPlayer) {
replay.subscribe(() => {
if (this.subscribers.size === 0) return;
let i = 0;
const index = Math.max(
0,
replay.stepper.text.findLastIndex((char) => {
if (char.source === "ghost") return false;
if (char.text === " " && i < 10) {
i++;
} else if (char.text === " ") {
return true;
}
return false;
}),
);
const length =
replay.stepper.text.length - replay.stepper.ghostCount - index;
const msPerChar =
((replay.stepper.text[
replay.stepper.text.length - replay.stepper.ghostCount - 1
]?.stamp ?? 0) -
(replay.stepper.text[index]?.stamp ?? 0)) /
length;
const value = 60_000 / (msPerChar * avgWordLength);
if (Number.isFinite(value)) {
for (const subscription of this.subscribers) {
subscription(value);
}
}
});
}
subscribe(subscription: (value: number) => void) {
this.subscribers.add(subscription);
return () => this.subscribers.delete(subscription);
}
}

View File

@@ -0,0 +1,23 @@
import type { ReplayPlayer } from "../player";
import type { ReplayPlugin, StoreContract } from "../types";
export class TextPlugin implements StoreContract<string>, ReplayPlugin {
private subscribers = new Set<(value: string) => void>();
register(replay: ReplayPlayer) {
replay.subscribe(() => {
if (this.subscribers.size === 0) return;
const text = replay.stepper.text
.filter((it) => it.source !== "ghost")
.map((it) => it.text)
.join("");
for (const subscription of this.subscribers) {
subscription(text);
}
});
}
subscribe(subscription: (value: string) => void) {
this.subscribers.add(subscription);
return () => this.subscribers.delete(subscription);
}
}

View File

@@ -0,0 +1,26 @@
import type { ReplayPlayer } from "../player";
import type { ReplayPlugin, StoreContract } from "../types";
export const avgWordLength = 5;
export class WpmReplayPlugin implements StoreContract<number>, ReplayPlugin {
private subscribers = new Set<(value: number) => void>();
register(replay: ReplayPlayer) {
replay.subscribe(() => {
if (this.subscribers.size === 0) return;
const msPerChar =
((replay.stepper.text.at(-1)?.stamp ?? 0) - replay.startTime) /
replay.stepper.text.length;
const value = 60_000 / (msPerChar * avgWordLength);
for (const subscription of this.subscribers) {
subscription(value);
}
});
}
subscribe(subscription: (value: number) => void) {
this.subscribers.add(subscription);
return () => this.subscribers.delete(subscription);
}
}

View File

@@ -0,0 +1,79 @@
import { ReplayPlayer } from "./player.js";
import type { Replay, ReplayEvent, TransmittableKeyEvent } from "./types.js";
function maybeRound<T>(value: T, round: boolean): T {
return typeof value === "number" && round ? (Math.round(value) as T) : value;
}
export class ReplayRecorder {
private held = new Map<string, [string, number]>();
private heldHandles = new Map<
string,
ReturnType<ReplayPlayer["playLiveEvent"]>
>();
replay: ReplayEvent[] = [];
private start = performance.now();
private isFirstPress = true;
player: ReplayPlayer;
constructor(challenge?: Replay["challenge"]) {
this.player = new ReplayPlayer({
start: this.start,
finish: this.start,
keys: [],
challenge,
});
}
next(event: TransmittableKeyEvent) {
if (this.isFirstPress) {
this.player.startTime = event.timeStamp;
this.isFirstPress = false;
}
this.player.replay.finish = event.timeStamp;
if (event.type === "keydown") {
this.held.set(event.code, [event.key, event.timeStamp]);
this.heldHandles.set(
event.code,
this.player.playLiveEvent(event.key, event.code),
);
} else {
const [key, start] = this.held.get(event.code) ?? ["", 0];
const delta = event.timeStamp - start;
this.held.delete(event.code);
const element = Object.freeze([key, event.code, start, delta] as const);
this.replay.push(element);
this.heldHandles.get(event.code)?.(delta);
this.heldHandles.delete(event.code);
}
}
finish(trim = true, round = true) {
return {
start: maybeRound(trim ? this.replay[0]?.[2] : this.start, round),
finish: maybeRound(
trim
? Math.max(...this.replay.map((it) => it[2] + it[3]))
: performance.now(),
round,
),
keys: this.replay
.map(
([key, code, at, duration]) =>
[
key,
code,
maybeRound(at, round),
maybeRound(duration, round),
] as const,
)
.sort((a, b) => a[2] - b[2]),
};
}
}

View File

@@ -0,0 +1,132 @@
import { ROBOT_THRESHOLD } from "./player";
import type { LiveReplayEvent, ReplayEvent, TextToken } from "./types";
/**
* This is the "heart" of the player logic
*/
export class ReplayStepper {
held = new Map<string, boolean>();
text: TextToken[];
cursor = 0;
challenge: TextToken[];
ghostCount: number;
mistakeCount = 0;
constructor(initialReplay: ReplayEvent[] = [], challenge = "") {
this.challenge = challenge.split("").map((text) => ({
stamp: 0,
duration: 0,
code: "",
text,
source: "ghost",
correct: true,
}));
this.text = [...this.challenge];
this.ghostCount = this.challenge.length;
for (const key of initialReplay) {
this.step(...key);
}
}
step(
...[output, code, at, duration]: ReplayEvent | LiveReplayEvent
): TextToken | undefined {
let token: TextToken | undefined = undefined;
if (output === "Backspace") {
if (this.held.has("ControlLeft") || this.held.has("ControlRight")) {
let wordIndex = 0;
for (let i = this.cursor - 1; i >= 0; i--) {
if (/\w+/.test(/** @type {TextToken} */ this.text[i]!.text)) {
wordIndex = i;
} else if (wordIndex !== 0) {
break;
}
}
this.text.splice(wordIndex, this.cursor - wordIndex);
} else if (this.cursor !== 0) {
this.text.splice(this.cursor - 1, 1);
}
this.cursor = Math.min(
this.cursor,
this.text.length - this.ghostCount + 1,
);
}
if (output.length === 1) {
token = {
stamp: at,
duration,
code,
text: output,
source:
duration === undefined
? undefined
: duration < ROBOT_THRESHOLD
? "robot"
: "human",
correct: true,
};
this.text.splice(this.cursor, 0, token);
}
if (code === "ArrowLeft" || code === "Backspace") {
this.cursor = Math.max(this.cursor - 1, 0);
}
if (code === "ArrowRight" || output.length === 1) {
this.cursor = Math.min(
this.cursor + 1,
this.text.length - this.ghostCount,
);
}
if (code === "Enter") {
token = {
stamp: at,
code,
duration,
text: "\n",
source:
duration === undefined
? undefined
: duration < ROBOT_THRESHOLD
? "robot"
: "human",
correct: true,
};
this.text.splice(this.cursor, 0, token);
this.cursor++;
}
if (this.challenge.length > 0) {
let challengeIndex = 0;
this.mistakeCount = 0;
for (let i = 0; i < this.text.length - this.ghostCount; i++) {
if (this.text[i]!.text === this.challenge[challengeIndex]?.text) {
this.text[i]!.correct = true;
} else {
this.mistakeCount++;
this.text[i]!.correct = false;
}
challengeIndex++;
}
const currentGhostCount = this.ghostCount;
this.ghostCount = Math.max(0, this.challenge.length - challengeIndex);
this.text.splice(
this.text.length - currentGhostCount,
Math.max(0, currentGhostCount - this.ghostCount),
...this.challenge.slice(
challengeIndex,
challengeIndex + Math.max(0, this.ghostCount - currentGhostCount),
),
);
}
return token;
}
}

View File

@@ -0,0 +1,58 @@
import { ReplayPlayer } from "./player.js";
export interface Replay {
start: number;
finish: number;
keys: ReplayEvent[];
challenge?: string;
}
export type LiveReplayEvent = readonly [
output: string,
code: string,
at: number,
];
export type ReplayEvent = readonly [...LiveReplayEvent, duration: number];
export interface TextToken {
stamp: number;
duration?: number;
text: string;
code: string;
source?: "human" | "robot" | "ghost";
correct: boolean;
}
export interface GraphData {
min: [number, number];
max: [number, number];
tokens: TextToken[][];
}
export interface ReplayStepResult {
text: TextToken[];
cursor: number;
challengeCursor: number;
token: TextToken | undefined;
}
export type TransmittableKeyEvent = Pick<
KeyboardEvent,
"timeStamp" | "type" | "code" | "key"
>;
export interface InferredChord {
id: number;
input: TextToken[];
output: string;
deviation: [number, number];
}
export interface ReplayPlugin {
register(replay: ReplayPlayer): void;
}
export interface StoreContract<T> {
subscribe(subscription: (value: T) => void): () => void;
set?: (value: T) => void;
}

View File

@@ -0,0 +1,96 @@
export const KBD_ICONS = new Map([
["KeyA", "a"],
["KeyB", "b"],
["KeyC", "c"],
["KeyD", "d"],
["KeyE", "e"],
["KeyF", "f"],
["KeyG", "g"],
["KeyH", "h"],
["KeyI", "i"],
["KeyJ", "j"],
["KeyK", "k"],
["KeyL", "l"],
["KeyM", "m"],
["KeyN", "n"],
["KeyO", "o"],
["KeyP", "p"],
["KeyQ", "q"],
["KeyR", "r"],
["KeyS", "s"],
["KeyT", "t"],
["KeyU", "u"],
["KeyV", "v"],
["KeyW", "w"],
["KeyX", "x"],
["KeyY", "y"],
["KeyZ", "z"],
["Digit0", "0"],
["Digit1", "1"],
["Digit2", "2"],
["Digit3", "3"],
["Digit4", "4"],
["Digit5", "5"],
["Digit6", "6"],
["Digit7", "7"],
["Digit8", "8"],
["Digit9", "9"],
["Period", "."],
["Comma", ","],
["Semicolon", ";"],
["Quote", "'"],
["BracketLeft", "["],
["BracketRight", "]"],
["Backslash", "\\"],
["Slash", "/"],
["Minus", "-"],
["Equal", "="],
["Backquote", "`"],
["IntlBackslash", "¦"],
["IntlRo", "ろ"],
["IntlYen", "¥"],
["IntlHash", "#"],
["BracketLeft", "["],
["BracketRight", "]"],
["NumLock", "⇭"],
["ScrollLock", "⇳"],
["Backspace", "⌫"],
["Delete", "⌦"],
["Enter", "↵"],
["Space", "␣"],
["Tab", "⇥"],
["ArrowLeft", "←"],
["ArrowRight", "→"],
["ArrowUp", "↑"],
["ArrowDown", "↓"],
["ShiftLeft", "⇧"],
["ShiftRight", "⇧"],
["ControlLeft", "Ctrl"],
["ControlRight", "Ctrl"],
["AltLeft", "Alt"],
["AltRight", "Alt"],
["MetaLeft", "⌘"],
["MetaRight", "⌘"],
["CapsLock", "⇪"],
["Escape", "Esc"],
["F1", "F1"],
["F2", "F2"],
["F3", "F3"],
["F4", "F4"],
["F5", "F5"],
["F6", "F6"],
["F7", "F7"],
["F8", "F8"],
["F9", "F9"],
["F10", "F10"],
["F11", "F11"],
["F12", "F12"],
["PrintScreen", "PrtSc"],
["Pause", "Pause"],
["Insert", "Ins"],
["Home", "Home"],
["End", "End"],
["PageUp", "PgUp"],
["PageDown", "PgDn"],
["ContextMenu", "Menu"],
]);

View File

@@ -0,0 +1,288 @@
import type { TextToken } from "../core/types";
import { KBD_ICONS } from "./kbd-icon";
export class TextRenderer {
shinyChords = true;
shiny: number[] | undefined;
readonly cursorNode: SVGRectElement;
private readonly nodes = new Map<TextToken, SVGTextElement>();
private readonly heldNodes = new Map<string, SVGTextElement>();
private readonly occupiedHeld: Array<boolean | undefined> = [];
private readonly occupied: number[] = [];
animationOptions: KeyframeAnimationOptions = {
duration: 100,
easing: "ease",
};
heldKeySize = 0.8;
ghostText = "";
constructor(
readonly node: HTMLElement,
readonly svg: SVGSVGElement,
readonly textNode: Text,
) {
this.cursorNode = document.createElementNS(
"http://www.w3.org/2000/svg",
"rect",
);
this.cursorNode.setAttribute("x", "0");
this.cursorNode.setAttribute("y", "0");
this.cursorNode.setAttribute("class", "cursor");
this.svg.appendChild(this.cursorNode);
}
set showCursor(value: boolean) {
this.cursorNode.style.visibility = value ? "visible" : "hidden";
}
getAtRange(i: number): [number, number] {
const range = document.createRange();
const rangeIndex = Math.max(0, Math.min(i, this.textNode.length - 1));
range.setStart(this.textNode, rangeIndex);
range.setEnd(
this.textNode,
this.textNode.length === 0 ? 0 : rangeIndex + 1,
);
const charBounds = range.getBoundingClientRect();
return [
i > this.textNode.length - 1
? charBounds.x + charBounds.width
: charBounds.x,
charBounds.y + charBounds.height / 2 + 1,
];
}
set held(keys: Map<string, boolean>) {
const prev = new Set(this.heldNodes.keys());
const fontSize = getComputedStyle(this.node).fontSize;
for (const [code, isHuman] of keys) {
if (!isHuman) continue;
prev.delete(code);
let node = this.heldNodes.get(code);
if (!node) {
let i = this.occupiedHeld.findIndex((it) => it === undefined);
if (i === -1) {
i = this.occupiedHeld.length;
this.occupiedHeld.push(true);
} else {
this.occupiedHeld[i] = true;
}
node = document.createElementNS("http://www.w3.org/2000/svg", "text");
node.textContent = KBD_ICONS.get(code) ?? null;
node.setAttribute("i", i.toString());
this.heldNodes.set(code, node);
node.style.transform = `${this.cursorNode.style.transform} translateY(calc(${fontSize} * ${
i + 1.5
}))`;
node.style.fontSize = `calc(${fontSize} * ${this.heldKeySize})`;
this.svg.appendChild(node);
node
.animate(
[
{
transform: `translateY(calc(-${fontSize} * ${this.heldNodes.size})) scale(0)`,
},
{ transform: "translateY(0px) scale(1)" },
],
{ duration: 200, composite: "add", easing: "ease-out" },
)
.play();
}
}
for (const code of prev) {
const node = this.heldNodes.get(code);
if (!node) continue;
this.heldNodes.delete(code);
this.occupiedHeld[Number(node.getAttribute("i"))] = undefined;
node
.animate(
[
{ transform: "translateX(0px)" },
{ transform: "translateX(-10px)" },
],
{
duration: 500,
composite: "accumulate",
easing: "ease-in",
},
)
.play();
const animation = node.animate([{ opacity: 1 }, { opacity: 0 }], {
duration: 500,
easing: "ease-in",
});
animation.onfinish = () => {
node.remove();
};
animation.play();
}
}
get animated(): boolean {
return this.cursorNode.classList.contains("animated");
}
set animated(value: boolean) {
if (value) {
this.cursorNode.classList.add("animated");
} else {
this.cursorNode.classList.remove("animated");
}
}
set cursor(cursor: number) {
const bounds = this.node.getBoundingClientRect();
const style = getComputedStyle(this.node);
const pos = this.getAtRange(cursor);
const x = pos[0] - bounds.x;
const y = pos[1] - bounds.y;
this.cursorNode.setAttribute("height", style.fontSize);
this.cursorNode.setAttribute("width", "1");
this.cursorNode.style.transform = `translate(${x}px, calc(${y}px - ${style.fontSize} / 2))`;
}
set text(text: TextToken[]) {
const prev = new Set(this.nodes.keys());
const bounds = this.node.getBoundingClientRect();
this.svg.setAttribute("width", bounds.width.toFixed(2));
this.svg.setAttribute("height", bounds.height.toFixed(2));
this.svg.setAttribute(
"viewBox",
`0 0 ${bounds.width.toFixed(2)} ${bounds.height.toFixed(2)}`,
);
text.forEach((token, i) => {
prev.delete(token);
let node = this.nodes.get(token);
const pos = this.getAtRange(i);
const x = pos[0] - bounds.x;
const y = pos[1] - bounds.y;
const xStr = x.toFixed(2);
const yStr = y.toFixed(2);
if (!node) {
node = document.createElementNS("http://www.w3.org/2000/svg", "text");
this.nodes.set(token, node);
this.svg.appendChild(node);
node.setAttribute("x", xStr);
node.setAttribute("y", yStr);
node.setAttribute("i", i.toString());
if (token.source === "ghost") {
node.setAttribute("opacity", "0.5");
}
this.occupied[i] ??= 0;
if (this.animated) {
if (this.occupied[i] > 0) {
node
.animate([{ opacity: 0 }, { opacity: 1 }], {
...this.animationOptions,
easing: "ease-out",
})
.play();
} else {
node
.animate(
[
{ opacity: 0, transform: "translateY(10px)" },
{ opacity: 1, transform: "translateY(0px)" },
],
{ ...this.animationOptions, easing: "ease-out" },
)
.play();
}
}
this.occupied[i]++;
}
if (!token.correct) {
node.setAttribute("incorrect", "");
} else {
node.removeAttribute("incorrect");
}
const prevX = node.getAttribute("x");
if (prevX && prevX !== xStr) {
const prev = parseFloat(prevX);
node.setAttribute("x", xStr);
/*if (this.animated) {
node.animate(
[{ transform: `translateX(${prev - x}px)` }, { transform: `translateX(0px)` }],
this.animationOptions
);
}*/
}
const prevY = node.getAttribute("y");
if (prevY && prevY !== yStr) {
const prev = parseFloat(prevY);
node.setAttribute("y", yStr);
/*if (this.animated) {
node.animate(
[{ transform: `translateY(${prev - y}px)` }, { transform: `translateY(0px)` }],
this.animationOptions
);
}*/
}
if (node.textContent !== token.text) {
node.textContent = token.text;
}
});
for (const token of prev) {
const node = this.nodes.get(token)!;
const i = parseInt(node.getAttribute("i")!);
this.nodes.delete(token);
if (this.animated) {
const animation = node.animate(
[{ opacity: 1 }, { opacity: 0 }],
this.animationOptions,
);
setTimeout(() => {
if (this.occupied[i] === 1) {
node
.animate(
[
{ transform: "translateY(0px)" },
{ transform: "translateY(10px)" },
],
this.animationOptions,
)
.play();
}
}, 10);
animation.onfinish = () => {
node.remove();
this.occupied[i]!--;
};
animation.play();
} else {
node.remove();
this.occupied[i]!--;
}
}
}
private isShiny(char: TextToken, index: number) {
return (
this.shiny?.includes(index) ||
(this.shinyChords && char.source === "robot")
);
}
}

View File

@@ -0,0 +1,71 @@
<script lang="ts">
import type { RoomMember } from "matrix-js-sdk";
import { matrixClient, memberColor } from "./chat";
import { theme } from "$lib/preferences";
import { hexFromArgb } from "@material/material-color-utilities";
let { members }: { members: RoomMember[] } = $props();
</script>
<div class="member-list">
{#each members as member (member.userId)}
{@const avatar = member.getMxcAvatarUrl()}
<div class="member">
{#if avatar}
<img
class="avatar"
src={$matrixClient.mxcUrlToHttp(avatar, 32, 32)}
alt={member.name}
width="32"
height="32"
/>
{:else}
{@const color = memberColor(member, $theme)}
{@const modeColor = $theme.mode === "dark" ? color.dark : color.light}
<div
style:background={hexFromArgb(modeColor.color)}
style:color={hexFromArgb(modeColor.onColor)}
class="avatar avatar-placeholder icon"
>
person
</div>
{/if}
<span>{member.name}</span>
</div>
{/each}
</div>
<style lang="scss">
.avatar {
border-radius: 50%;
width: 32px;
height: 32px;
flex-shrink: 0;
}
.avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.member {
display: flex;
align-items: center;
gap: 0.5rem;
}
.member-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 8px;
overflow-y: auto;
height: 100%;
}
span {
word-break: break-all;
}
</style>

View File

@@ -0,0 +1,73 @@
<script lang="ts">
import type { Room } from "matrix-js-sdk";
import { matrixClient, currentRoomId } from "./chat";
let { rooms }: { rooms: Room[] } = $props();
</script>
<div class="rooms">
{#each $matrixClient.getRooms() as room}
{@const avatar = room.getMxcAvatarUrl()}
<button
class:active={$currentRoomId === room.roomId}
class="room"
onclick={() => ($currentRoomId = room.roomId)}
>
{#if avatar}
<img
alt={room.name}
src={$matrixClient.mxcUrlToHttp(avatar, 16, 16)}
width="16"
height="16"
/>
{:else}
<div>#</div>
{/if}
<div>{room.name}</div>
</button>
{/each}
{#await $matrixClient.publicRooms()}
<div>Loading...</div>
{:then rooms}
{#each rooms.chunk as room}
<button class="room" onclick={() => ($currentRoomId = room.roomId)}>
<div>#</div>
<div>{room.name}</div>
</button>
{/each}
{:catch error}
<div>{error.message}</div>
{/await}
</div>
<style lang="scss">
.rooms {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
padding-left: 0;
width: 100%;
}
.room {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.5rem;
cursor: pointer;
padding-block: 2px;
min-height: 0;
height: unset;
padding-inline: 16px;
padding-block: 4px;
border-radius: 8px;
width: 100%;
&.active {
background: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
}
}
</style>

View File

@@ -0,0 +1,231 @@
<script lang="ts">
import type {
EventTimeline,
MatrixEvent,
MsgType,
Room,
RoomEvent,
RoomMember,
RoomMemberEvent,
} from "matrix-js-sdk";
import { onDestroy, onMount, tick } from "svelte";
import { matrixClient } from "./chat";
import MatrixEventComponent from "./events/MatrixEvent.svelte";
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
import { ReplayRecorder } from "$lib/charrecorder/core/recorder";
import { type Socket, io } from "socket.io-client";
import { SvelteMap } from "svelte/reactivity";
let { timeline }: { timeline: EventTimeline } = $props();
const excludeEvents = ["m.reaction", "m.room.redaction"];
let events = $state(
timeline
.getEvents()
.filter((it) => !excludeEvents.includes(it.getType()))
.reverse(),
);
let recorder = $state(new ReplayRecorder());
let showCursor = $state(false);
let timelineElement: HTMLElement = $state()!;
async function onTimeline(
event: MatrixEvent,
room?: Room,
toStartOfTimeline?: boolean,
) {
if (room?.roomId !== timeline.getRoomId()) return;
const sender = event.getSender();
if (sender) {
live.delete(sender);
}
if (excludeEvents.includes(event.getType())) return;
if (toStartOfTimeline) {
events.push(event);
} else {
const needScroll = timelineElement.scrollTop < 20;
events.unshift(event);
if (needScroll) {
await tick();
timelineElement.scroll({
top: 0,
behavior: "smooth",
});
}
}
}
let typing = $state<string[]>([]);
function onTyping(event: MatrixEvent, member: RoomMember) {
typing = event.event.content?.["user_ids"] ?? [];
}
async function send() {
const roomId = timeline.getRoomId();
if (!roomId) return;
const finalText = recorder.player.stepper.text
.map((token) => token.text)
.join("");
const finalRecording = recorder.finish();
if (!finalText) return;
recorder = new ReplayRecorder();
await $matrixClient.sendMessage(roomId, {
msgtype: "m.text" as MsgType.Text,
body: finalText,
// @ts-expect-error
"m.replay": finalRecording,
});
}
function onKey(event: KeyboardEvent) {
if (event.type === "keyup" && event.key === "Enter" && !event.shiftKey) {
send();
return;
} else {
recorder.next(event);
}
if (event.type === "keyup" && recorder.player.stepper.text.length === 0) {
recorder = new ReplayRecorder();
} else {
socket.emit("message", {
timeStamp: event.timeStamp,
type: event.type,
key: event.key,
code: event.code,
username: $matrixClient.getUserId(),
});
}
}
let socket: Socket = $state()!;
let live = new SvelteMap<string, ReplayRecorder>();
onMount(() => {
socket = io("https://srv.charachorder.io");
socket.emit("join", timeline.getRoomId());
socket.on("message", async ({ message }) => {
let userRecorder = live.get(message.username);
if (!userRecorder) {
userRecorder = new ReplayRecorder();
live.set(message.username, userRecorder);
}
await tick();
userRecorder.next(message);
if (userRecorder.player.stepper.text.length === 0) {
live.delete(message.username);
}
});
$matrixClient.on("Room.timeline" as RoomEvent.Timeline, onTimeline);
$matrixClient.on("RoomMember.typing" as RoomMemberEvent.Typing, onTyping);
});
onDestroy(() => {
socket?.disconnect();
$matrixClient.off("Room.timeline" as RoomEvent.Timeline, onTimeline);
$matrixClient.off("RoomMember.typing" as RoomMemberEvent.Typing, onTyping);
});
</script>
<section>
<div bind:this={timelineElement} class="timeline">
{#each live.entries() as [userId, recorder] (userId)}
{@const roomId = timeline.getRoomId()}
{#if roomId}
{@const room = $matrixClient.getRoom(roomId)}
{@const member = room?.getMember(userId)}
{#if member}
<MatrixEventComponent sender={member} replay={recorder.player} />
{/if}
{/if}
{/each}
{#each events as event, i (event.event["event_id"])}
{@const prev = events[i + 1]}
<MatrixEventComponent {event} sender={event.sender} {prev} {timeline} />
{/each}
</div>
<div class="static-elements">
<div class="indicators"></div>
<div class="input-box">
<button class="icon">add</button>
<div
role="textbox"
tabindex="0"
class="input"
onkeydown={onKey}
onkeyup={onKey}
onfocusin={() => (showCursor = true)}
onfocusout={() => (showCursor = false)}
>
<CharRecorder replay={recorder.player} cursor={showCursor} />
</div>
<button class="icon" onclick={send}>send</button>
</div>
</div>
</section>
<style lang="scss">
$border-radius: 16px;
.input {
border: 1px solid var(--md-sys-color-outline);
flex-grow: 1;
cursor: text;
padding: 0.5em;
font-size: 1rem;
border-radius: $border-radius;
text-wrap: wrap;
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-word;
&:focus-visible {
outline: none;
}
}
.input-box {
display: flex;
gap: 4px;
padding-block: 8px;
flex-shrink: 0;
width: 100%;
}
.static-elements {
position: relative;
width: 100%;
}
.timeline {
contain: content;
height: auto;
display: flex;
flex-direction: column-reverse;
overflow-y: scroll;
overflow-x: hidden;
flex-grow: 1;
width: 100%;
}
section {
display: flex;
flex-direction: column;
overflow: hidden;
justify-content: flex-end;
width: 100%;
height: 100%;
}
</style>

109
src/lib/chat/chat-rx.ts Normal file
View File

@@ -0,0 +1,109 @@
import { derived, writable, type Writable } from "svelte/store";
import type {
ClientEvent,
LoginResponse,
MatrixClient,
RoomMember,
} from "matrix-js-sdk";
import { persistentWritable } from "$lib/storage";
import {
themeFromSourceColor,
argbFromHex,
type CustomColorGroup,
} from "@material/material-color-utilities";
import type { UserTheme } from "$lib/preferences";
import { MatrixRx } from "./matrix-rx/client";
export const matrixClient: Writable<MatrixClient> = writable();
export const isLoggedIn: Writable<boolean> = writable(false);
export const matrix = derived(
[matrixClient, isLoggedIn],
([matrixClient, isLoggedIn]) =>
isLoggedIn ? new MatrixRx(matrixClient) : undefined,
);
export const currentRoomId = persistentWritable<string | null>(
"currentRoomId",
null,
);
function getStoredLogin(): LoginResponse | undefined {
try {
return JSON.parse(localStorage.getItem("matrix-login")!);
} catch {
return undefined;
}
}
export function storeLogin(response: LoginResponse) {
localStorage.setItem("matrix-login", JSON.stringify(response));
}
export async function initMatrixClient() {
const { createClient, IndexedDBStore, IndexedDBCryptoStore } = await import(
"matrix-js-sdk"
);
const storedLogin = getStoredLogin();
const store = new IndexedDBStore({
dbName: "matrix",
indexedDB: window.indexedDB,
});
const cryptoStore = new IndexedDBCryptoStore(
window.indexedDB,
"matrix-crypto",
);
const client = createClient({
baseUrl: import.meta.env.VITE_MATRIX_URL,
userId: storedLogin?.user_id,
accessToken: storedLogin?.access_token,
timelineSupport: true,
store,
cryptoStore,
});
console.log("store");
await store.startup();
console.log("cryptoStore");
await cryptoStore.startup();
console.log("client");
await client.startClient();
client.once("sync" as ClientEvent.Sync, () => {
isLoggedIn.set(client.isLoggedIn());
});
const loginToken = new URLSearchParams(window.location.search).get(
"loginToken",
);
if (loginToken) {
storeLogin(await client.loginWithToken(loginToken));
window.history.replaceState({}, document.title, window.location.pathname);
isLoggedIn.set(client.isLoggedIn());
}
matrixClient.set(client);
console.log("done");
}
export function memberColor(
member: RoomMember,
theme: UserTheme,
): CustomColorGroup {
let hash = 0;
member.userId.split("").forEach((char) => {
hash = char.charCodeAt(0) + ((hash << 5) - hash);
});
let color = "#";
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += value.toString(16).padStart(2, "0");
}
return themeFromSourceColor(argbFromHex(theme.color), [
{ value: argbFromHex(color), name: "member", blend: true },
]).customColors.find((c) => c.color.name === "member")!;
}

35
src/lib/chat/chat.ts Normal file
View File

@@ -0,0 +1,35 @@
import { writable, type Writable } from "svelte/store";
import type { MatrixClient, RoomMember } from "matrix-js-sdk";
import { persistentWritable } from "$lib/storage";
import {
themeFromSourceColor,
argbFromHex,
type CustomColorGroup,
} from "@material/material-color-utilities";
import type { UserTheme } from "$lib/preferences";
export const matrixClient: Writable<MatrixClient> = writable();
export const currentRoomId = persistentWritable<string | null>(
"currentRoomId",
null,
);
export function memberColor(
member: RoomMember,
theme: UserTheme,
): CustomColorGroup {
let hash = 0;
member.userId.split("").forEach((char) => {
hash = char.charCodeAt(0) + ((hash << 5) - hash);
});
let color = "#";
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += value.toString(16).padStart(2, "0");
}
return themeFromSourceColor(argbFromHex(theme.color), [
{ value: argbFromHex(color), name: "member", blend: true },
]).customColors.find((c) => c.color.name === "member")!;
}

View File

@@ -0,0 +1,381 @@
<script lang="ts">
import type {
EventTimeline,
MatrixEvent,
MatrixEventEvent,
Relations,
RelationsEvent,
RoomMember,
} from "matrix-js-sdk";
import MatrixMessageEvent from "./MatrixMessageEvent.svelte";
import { matrixClient, memberColor } from "../chat";
import { theme } from "$lib/preferences";
import { hexFromArgb } from "@material/material-color-utilities";
import { fade } from "svelte/transition";
import type { Replay } from "$lib/charrecorder/core/types";
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
import type { ReplayPlayer } from "$lib/charrecorder/core/player";
import { onDestroy, onMount } from "svelte";
import { writable } from "svelte/store";
let {
event,
prev,
sender,
replay: replayPlayer,
timeline,
}: {
event?: MatrixEvent;
prev?: MatrixEvent;
sender?: RoomMember | null;
replay?: Replay | ReplayPlayer;
timeline?: EventTimeline;
} = $props();
let toolbarHover = $state(false);
let mainHover = $state(false);
let hover = $derived(toolbarHover || mainHover);
let replay: Replay | undefined = $state();
let reactions: Relations | undefined = $state(
timeline && event?.event.event_id
? timeline
.getTimelineSet()
.relations.getChildEventsForEvent(
event.event.event_id,
"m.annotation",
"m.reaction",
)
: undefined,
);
let annotations = writable<[string, Set<MatrixEvent>][] | null | undefined>();
function createRelations() {
if (!timeline || !event?.event.event_id) return;
reactions?.off("Relations.add" as RelationsEvent.Add, createRelations);
reactions?.off(
"Relations.remove" as RelationsEvent.Remove,
createRelations,
);
reactions = timeline
.getTimelineSet()
.relations.getChildEventsForEvent(
event.event.event_id,
"m.annotation",
"m.reaction",
);
reactions?.on("Relations.add" as RelationsEvent.Add, createRelations);
reactions?.on("Relations.remove" as RelationsEvent.Remove, createRelations);
reactions?.on(
"Relations.redaction" as RelationsEvent.Redaction,
createRelations,
);
annotations.set(
reactions?.getSortedAnnotationsByKey()?.filter(([, it]) => it.size > 0),
);
console.log("create");
}
onMount(() => {
createRelations();
event?.on(
"Event.relationsCreated" as MatrixEventEvent.RelationsCreated,
createRelations,
);
});
onDestroy(() => {
event?.off(
"Event.relationsCreated" as MatrixEventEvent.RelationsCreated,
createRelations,
);
reactions?.off("Relations.add" as RelationsEvent.Remove, createRelations);
reactions?.off(
"Relations.remove" as RelationsEvent.Remove,
createRelations,
);
reactions?.off(
"Relations.redaction" as RelationsEvent.Redaction,
createRelations,
);
});
</script>
<div
class="event"
role="log"
onmouseover={() => (mainHover = true)}
onfocus={() => (mainHover = true)}
onmouseout={() => (mainHover = false)}
onblur={() => (mainHover = false)}
>
{#if event && hover}
<div class="backdrop" transition:fade={{ duration: 100 }}></div>
{/if}
{#if sender && !(prev && prev?.getType() === event?.getType() && prev.sender?.userId === event.sender?.userId)}
{@const color = memberColor(sender, $theme)}
{@const avatarMxc = sender.getMxcAvatarUrl()}
{#if avatarMxc}
{@const avatar = $matrixClient.mxcUrlToHttp(avatarMxc, 32, 32)}
<img
class="avatar"
src={avatar}
alt={sender.name}
width="32"
height="32"
/>
{:else}
<div
class="avatar avatar-placeholder icon"
style:background={hexFromArgb(
$theme.mode === "dark" ? color.dark.color : color.light.color,
)}
style:color={hexFromArgb(
$theme.mode === "dark" ? color.dark.onColor : color.light.onColor,
)}
>
person
</div>
{/if}
<div
class="sender"
style:color={hexFromArgb(
$theme.mode === "dark" ? color.dark.color : color.light.color,
)}
>
<strong>{sender.name}</strong>
{#if replay || replayPlayer}
<div class="dots">
{#each new Array(3) as _, i}
<div
style:animation-delay={i * 0.2 + "s"}
style:background={hexFromArgb(
$theme.mode === "dark" ? color.dark.color : color.light.color,
)}
class="dot"
></div>
{/each}
</div>
{/if}
</div>
{/if}
<div class="content">
{#if event}
{#if event.getType() === "m.room.message"}
<MatrixMessageEvent {event} bind:replay />
{:else}
<details>
<summary>{event.getType()}</summary>
<pre>{JSON.stringify(event.event, null, 2)}</pre>
</details>
{/if}
{/if}
{#if replayPlayer}
<CharRecorder replay={replayPlayer} cursor={true} keys={true} />
{/if}
</div>
{#if event && hover}
<div
role="toolbar"
tabindex="0"
class="toolbar"
transition:fade={{ duration: 100 }}
onmouseover={() => (toolbarHover = true)}
onfocus={() => (toolbarHover = true)}
onmouseout={() => (toolbarHover = false)}
onblur={() => (toolbarHover = false)}
>
{#if event.getType() === "m.room.message"}
{@const message = event.event.content?.["body"]}
<a
class="icon rocket"
href="/learn/sentence/?sentence={encodeURIComponent(message)}"
>rocket_launch</a
>
{/if}
<button class="icon">add_reaction</button>
<button class="icon">reply</button>
{#if event.event.content?.["m.replay"]}
{#if replay}
<button class="icon" onclick={() => (replay = undefined)}>stop</button
>
{:else}
<button
class="icon"
onclick={() => (replay = event.event.content?.["m.replay"])}
>replay</button
>
{/if}
{/if}
<button class="icon">more_horiz</button>
</div>
{/if}
{#if $annotations && $annotations.length > 0}
<div class="reactions">
{#each $annotations as [reaction, events]}
<button class="reaction"
>{reaction} <span class="count">{events.size}</span></button
>
{/each}
</div>
{/if}
</div>
<style lang="scss">
details {
opacity: 0.5;
word-wrap: break-word;
}
pre {
text-wrap: wrap;
word-wrap: break-word;
}
@keyframes rocket {
0% {
transform: translate(0, 0);
}
90% {
transform: translate(4px, -4px);
}
100% {
transform: translate(0, 0);
}
}
.icon.rocket {
animation: rocket 2s;
}
.toolbar {
position: absolute;
top: -26px;
right: 0;
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
padding: 4px;
border-radius: 4px;
display: flex;
z-index: 100;
a,
button {
font-size: 16px;
width: 24px;
height: 24px;
}
}
.dots {
display: flex;
gap: 2px;
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-4px);
}
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
animation: bounce 1s infinite;
}
.sender,
.avatar {
margin-block: 2px 4px;
}
.avatar {
grid-area: avatar;
width: 32px;
height: 32px;
border-radius: 50%;
translate: 0 2px;
}
div.avatar {
display: flex;
justify-content: center;
align-items: center;
}
.sender {
display: flex;
grid-area: sender;
align-items: center;
gap: 8px;
}
.reactions {
grid-area: reactions;
margin-top: 2px;
display: flex;
gap: 4px;
}
.reaction {
border: 1px solid var(--md-sys-color-outline);
padding: 6px;
border-radius: 6px;
height: 24px;
display: flex;
font-size: 12px;
> .count {
font-size: 10px;
}
}
.event {
display: grid;
position: relative;
padding-inline: 0.5em;
margin-inline: 0.5em;
padding-block: 0.25em;
border-radius: 4px;
grid-template-areas:
"avatar sender date"
"avatar content content"
"none reactions reactions";
grid-template-columns: 32px 1fr auto;
}
.content {
grid-area: content;
text-wrap: wrap;
word-wrap: break-word;
}
.reactions,
.content,
.sender {
margin-inline: 8px;
}
.backdrop {
position: absolute;
inset: 0;
z-index: -1;
opacity: 0.25;
background: var(--md-sys-color-surface-variant);
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,56 @@
<script lang="ts">
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
import type { Replay } from "$lib/charrecorder/core/types";
import type { MatrixClient, MatrixEvent } from "matrix-js-sdk";
import { fade } from "svelte/transition";
import { matrixClient } from "../chat";
let { event, replay = $bindable() }: { event: MatrixEvent; replay?: Replay } =
$props();
</script>
<div>
{#if event.event.content?.msgtype === "m.image"}
<img
src={$matrixClient.mxcUrlToHttp(event.event.content["url"])}
alt={event.event.content["body"]}
/>
{:else}
<span class="content" style:opacity={replay && 0}
>{event.event.content?.["body"]}</span
>
{/if}
{#if replay}
<div class="replay" out:fade>
<CharRecorder
{replay}
cursor={true}
keys={true}
ondone={() => (replay = undefined)}
/>
</div>
{/if}
</div>
<style lang="scss">
div {
position: relative;
min-height: 1.5em;
}
img {
max-width: 100%;
max-height: 16em;
border-radius: 8px;
}
.content {
transition: opacity 0.2s;
}
.replay {
position: absolute;
top: 0;
left: 0;
}
</style>

View File

@@ -0,0 +1,71 @@
import type { Direction, MatrixClient, Room } from "matrix-js-sdk";
import {
filter,
map,
type Observable,
of,
distinctUntilChanged,
merge,
} from "rxjs";
import { fromMatrixClientEvent } from "./events";
function roomListDistinct(prev: Room[], curr: Room[]) {
if (prev.length !== curr.length) return false;
for (let i = 0; i < prev.length; i++) {
if (prev[i]!.roomId !== curr[i]!.roomId) return false;
}
return true;
}
export class MatrixRx {
topLevelRooms$: Observable<Room[]>;
topLevelSpaces$: Observable<Room[]>;
topLevelChats$: Observable<Room[]>;
constructor(private client: MatrixClient) {
this.topLevelRooms$ = merge(
of([]),
fromMatrixClientEvent(client, "Room"),
fromMatrixClientEvent(client, "deleteRoom"),
fromMatrixClientEvent(client, "Room.myMembership"),
fromMatrixClientEvent(client, "Room.CurrentStateUpdated").pipe(
filter(
([_room, prev, curr]) =>
prev.getStateEvents("m.space.parent").length !==
curr.getStateEvents("m.space.parent").length,
),
),
).pipe(
map(() =>
this.client.getVisibleRooms().filter(
(room) =>
room.getMyMembership() !== "leave" &&
room
.getLiveTimeline()
.getState("f" as Direction.Forward)
?.getStateEvents("m.space.parent").length === 0,
),
),
distinctUntilChanged(roomListDistinct),
);
this.topLevelSpaces$ = this.topLevelRooms$.pipe(
map((rooms) => rooms.filter((room) => room.isSpaceRoom())),
distinctUntilChanged(roomListDistinct),
);
this.topLevelChats$ = this.topLevelRooms$.pipe(
map((rooms) => rooms.filter((room) => !room.isSpaceRoom())),
distinctUntilChanged(roomListDistinct),
);
}
}
export class SpaceRx {
constructor(
private client: MatrixClient,
private space: Room,
) {}
}

View File

@@ -0,0 +1,11 @@
import type { ClientEventHandlerMap, MatrixClient } from "matrix-js-sdk";
import { fromEvent, type Observable } from "rxjs";
export function fromMatrixClientEvent<T extends keyof ClientEventHandlerMap>(
client: MatrixClient,
eventName: `${T}`, // hack so we can use strings instead of enums
): Observable<Parameters<ClientEventHandlerMap[T]>> {
return fromEvent(client, eventName) as Observable<
Parameters<ClientEventHandlerMap[T]>
>;
}

View File

@@ -0,0 +1,85 @@
import type {
MatrixClient,
MatrixEvent,
Room,
Direction,
RoomState,
RoomStateEventHandlerMap,
EventType,
} from "matrix-js-sdk";
import { fromMatrixClientEvent } from "./events";
import {
map,
filter,
merge,
startWith,
Observable,
of,
fromEvent,
concat,
defer,
} from "rxjs";
export function matrixRoom$(
client: MatrixClient,
roomId: string | undefined,
): Observable<Room | undefined> {
return merge([
fromMatrixClientEvent(client, "Room").pipe(
filter(([room]) => room.roomId === roomId),
),
fromMatrixClientEvent(client, "deleteRoom").pipe(
filter(([id]) => id === roomId),
),
]).pipe(
startWith([]),
map(() => client.getRoom(roomId) ?? undefined),
);
}
export function roomTimeline$(
client: MatrixClient,
room: Room | undefined,
): Observable<MatrixEvent[] | undefined> {
if (!room) return of(undefined);
const eventTimeline = room.getLiveTimeline();
return fromMatrixClientEvent(client, "Room.timeline").pipe(
filter(
([, eventRoom]) =>
eventRoom !== undefined && eventRoom.roomId === room.roomId,
),
startWith([]),
map(() => eventTimeline.getEvents()),
);
}
export function roomCurrentStateEvents$(
client: MatrixClient,
room: Room,
eventType: EventType | string,
): Observable<MatrixEvent[]> {
return concat(
defer(() =>
of(
room
.getLiveTimeline()
.getState("f" as Direction.Forward)
?.getStateEvents(eventType) ?? [],
),
),
fromMatrixClientEvent(client, "Room.CurrentStateUpdated").pipe(
filter(([room]) => room.roomId === room.roomId),
map(([_room, _prev, curr]) => curr.getStateEvents(eventType)),
),
);
}
export function fromRoomStateEvent<T extends keyof RoomStateEventHandlerMap>(
state: RoomState,
eventName: `${T}`,
): Observable<Parameters<RoomStateEventHandlerMap[T]>> {
return fromEvent(state, eventName) as Observable<
Parameters<RoomStateEventHandlerMap[T]>
>;
}

Some files were not shown because too many files have changed in this diff Show More