16 Commits

Author SHA1 Message Date
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
64 changed files with 7670 additions and 11923 deletions

View File

@@ -21,18 +21,22 @@ jobs:
- name: ⏬ Install Python dependencies - name: ⏬ Install Python dependencies
run: pip install -r requirements.txt run: pip install -r requirements.txt
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- name: 🐉 Use Node.js 18.16.x - name: 🐉 Use Node.js 18.16.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.16.x node-version: 18.16.x
cache: "npm" cache: "pnpm"
- name: ⏬ Install Node dependencies - name: ⏬ Install Node dependencies
run: npm ci run: pnpm install
- name: 🔥 Optimize icon font - name: 🔥 Optimize icon font
run: npm run minify-icons run: pnpm minify-icons
- name: 🔨 Build site - name: 🔨 Build site
run: npm run build run: pnpm build
- name: 📦 Upload build artifacts - name: 📦 Upload build artifacts
uses: actions/upload-artifact@v3.1.2 uses: actions/upload-artifact@v3.1.2

View File

@@ -29,7 +29,7 @@ You may need to run through some additional setup to get Rust running inside Int
- Python >=3.10 - Python >=3.10
- Rust Stable (For Tauri Development) - Rust Stable (For Tauri Development)
I know, python in JS projects is extremely annoying, unfortunately, I know, python in JS projects is extremely annoying. Unfortunately,
it seems to be the only platform that offers a functional it seems to be the only platform that offers a functional
way to subset variable woff2 fonts with ligatures. way to subset variable woff2 fonts with ligatures.

117
flake.nix
View File

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

View File

@@ -112,6 +112,7 @@ const config = {
upload_2: "ff52", upload_2: "ff52",
stat_minus_2: "e69c", stat_minus_2: "e69c",
stat_2: "e699", stat_2: "e699",
routine: "e20c",
}, },
}; };

11684
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,12 @@
{ {
"name": "charachorder-device-manager", "name": "charachorder-device-manager",
"version": "1.5.0", "version": "1.5.2",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"private": true, "private": true,
"engines": {
"node": ">=18.16",
"pnpm": ">=8.6"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/CharaChorder/DeviceManager.git" "url": "https://github.com/CharaChorder/DeviceManager.git"
@@ -30,52 +34,55 @@
"typesafe-i18n": "typesafe-i18n" "typesafe-i18n": "typesafe-i18n"
}, },
"devDependencies": { "devDependencies": {
"@codemirror/autocomplete": "^6.15.0", "@codemirror/autocomplete": "^6.17.0",
"@codemirror/commands": "^6.3.3", "@codemirror/commands": "^6.6.0",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/language": "^6.10.1", "@codemirror/language": "^6.10.2",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@fontsource-variable/material-symbols-rounded": "^5.0.27", "@codemirror/view": "^6.28.4",
"@fontsource-variable/noto-sans-mono": "^5.0.19", "@fontsource-variable/material-symbols-rounded": "^5.0.34",
"@material/material-color-utilities": "^0.2.7", "@fontsource-variable/noto-sans-mono": "^5.0.20",
"@lezer/highlight": "^1.2.0",
"@material/material-color-utilities": "^0.3.0",
"@modyfi/vite-plugin-yaml": "^1.1.0", "@modyfi/vite-plugin-yaml": "^1.1.0",
"@sveltejs/adapter-static": "^2.0.3", "@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/kit": "^1.30.4", "@sveltejs/kit": "^2.5.18",
"@sveltejs/vite-plugin-svelte": "^2.5.3", "@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tauri-apps/api": "^1.5.3", "@tauri-apps/api": "^1.6.0",
"@tauri-apps/cli": "^1.5.11", "@tauri-apps/cli": "^1.6.0",
"@types/dom-view-transitions": "^1.0.4", "@types/dom-view-transitions": "^1.0.4",
"@types/flexsearch": "^0.7.6", "@types/flexsearch": "^0.7.6",
"@types/w3c-web-serial": "^1.0.6", "@types/w3c-web-serial": "^1.0.6",
"@types/w3c-web-usb": "^1.0.10", "@types/w3c-web-usb": "^1.0.10",
"@vite-pwa/sveltekit": "^0.2.10", "@vite-pwa/sveltekit": "^0.6.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"cypress": "^13.7.2", "cypress": "^13.13.0",
"flexsearch": "^0.7.43", "flexsearch": "^0.7.43",
"fontkit": "^2.0.2", "fontkit": "^2.0.2",
"glob": "^10.3.12", "glob": "^10.4.3",
"jsdom": "^22.1.0", "jsdom": "^24.1.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.2.5", "prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.2", "prettier-plugin-svelte": "^3.2.5",
"sass": "^1.74.1", "sass": "^1.77.6",
"stylelint": "^15.11.0", "stylelint": "^16.6.1",
"stylelint-config-clean-order": "^5.4.2", "stylelint-config-clean-order": "^6.1.0",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-prettier-scss": "^1.0.0", "stylelint-config-prettier-scss": "^1.0.0",
"stylelint-config-recommended-scss": "^13.1.0", "stylelint-config-recommended-scss": "^14.0.0",
"stylelint-config-standard-scss": "^11.1.0", "stylelint-config-standard-scss": "^13.1.0",
"svelte": "^4.2.12", "svelte": "^4.2.18",
"svelte-check": "^3.6.9", "svelte-check": "^3.8.4",
"svelte-preprocess": "^5.1.3", "svelte-preprocess": "^6.0.1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"typesafe-i18n": "^5.26.2", "typesafe-i18n": "^5.26.2",
"typescript": "^5.4.4", "typescript": "^5.5.3",
"vite": "^4.5.3", "vite": "^5.3.3",
"vite-plugin-mkcert": "^1.17.5", "vite-plugin-mkcert": "^1.17.5",
"vite-plugin-pwa": "^0.17.5", "vite-plugin-pwa": "^0.20.0",
"vitest": "^0.34.6" "vitest": "^1.6.0",
"workbox-window": "^7.1.0"
}, },
"type": "module" "type": "module"
} }

7074
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "app" name = "app"
version = "1.5.0" version = "1.5.2"
description = "A Tauri App" description = "A Tauri App"
authors = ["Thea Schöbl <dev@theaninova.de>"] authors = ["Thea Schöbl <dev@theaninova.de>"]
license = "AGPL-3" license = "AGPL-3"

View File

@@ -6,7 +6,7 @@
"devPath": "http://localhost:5173", "devPath": "http://localhost:5173",
"distDir": "../build" "distDir": "../build"
}, },
"package": { "productName": "amacc1ng", "version": "1.5.0" }, "package": { "productName": "amacc1ng", "version": "1.5.2" },
"tauri": { "tauri": {
"allowlist": { "all": false }, "allowlist": { "all": false },
"bundle": { "bundle": {

View File

@@ -1,6 +1,9 @@
name: CharaChorder name: CharaChorder
description: CharaChorder specific actions description: CharaChorder specific actions
actions: actions:
0:
id: "NO_ACTION"
display: "No Action"
528: 528:
id: "RESTART" id: "RESTART"
title: Restart Device title: Restart Device
@@ -58,6 +61,7 @@ actions:
544: 544:
variantOf: 36 variantOf: 36
id: "SPACERIGHT" id: "SPACERIGHT"
display: " "
title: Right Spacebar (eg CC Lite) title: Right Spacebar (eg CC Lite)
icon: space_bar icon: space_bar
variant: right variant: right
@@ -66,6 +70,9 @@ actions:
title: Primary Keymap title: Primary Keymap
icon: counter_1 icon: counter_1
variant: left variant: left
description: |
Acts as a toggle if the same action is not assigned
to the target layer
549: 549:
variantOf: 548 variantOf: 548
<<: *primary_keymap <<: *primary_keymap
@@ -76,6 +83,9 @@ actions:
title: Numeric Layer title: Numeric Layer
icon: counter_2 icon: counter_2
variant: left variant: left
description: |
Acts as a toggle if the same action is not assigned
to the target layer
551: 551:
variantOf: 550 variantOf: 550
<<: *secondary_keymap <<: *secondary_keymap
@@ -86,11 +96,34 @@ actions:
title: Function Layer title: Function Layer
icon: counter_3 icon: counter_3
variant: left variant: left
description: |
Acts as a toggle if the same action is not assigned
to the target layer
553: 553:
variationOf: 552 variationOf: 552
<<: *tertiary_keymap <<: *tertiary_keymap
id: "KM_3_R" id: "KM_3_R"
variant: right variant: right
558:
id: HOLD_COMPOUND
title: Activate Chord Library
icon: layers
description: |
When used in a chord includes that chord as a base
compound chord for all subsequent chords.
This is effectively a library switch.
Since library activations can be nested, you
usually add a "Reset Chord Library" before this action.
559:
id: RELEASE_COMPOUND
title: Reset Chord Library
icon: layers_clear
description: |
Releases the active compound state, returning
to the default library.
While "Activate Chord Library" can only be used
as an output of a chord, this action can be assigned
to switches directly.
576: 576:
id: ACTION_DELAY_1000 id: ACTION_DELAY_1000
icon: clock_loader_90 icon: clock_loader_90

View File

@@ -422,8 +422,8 @@ actions:
title: Keyboard Non-US \ and | (US English) title: Keyboard Non-US \ and | (US English)
357: 357:
id: "COMPOSE" id: "COMPOSE"
icon: menu
title: Keyboard Application title: Keyboard Application
description: Officially supported by Win, Unix, and Boot
358: 358:
id: "POWER" id: "POWER"
keyCode: "Power" keyCode: "Power"
@@ -944,99 +944,99 @@ actions:
title: Keyboard Right GUI title: Keyboard Right GUI
488: 488:
id: "KSC_E8" id: "KSC_E8"
icon: play_pause
keyCode: "MediaPlayPause" keyCode: "MediaPlayPause"
title: Media Play Pause title: Media Play Pause
description: Not required to be supported by any OS. Possibly deprecated.
489: 489:
id: "KSC_E9" id: "KSC_E9"
icon: stop
keyCode: "MediaStop" keyCode: "MediaStop"
title: Media Stop CD title: Media Stop CD
description: Not required to be supported by any OS. Possibly deprecated.
490: 490:
id: "KSC_EA" id: "KSC_EA"
icon: skip_previous
keyCode: "MediaTrackPrevious" keyCode: "MediaTrackPrevious"
title: Media Previous Song title: Media Previous Song
description: Not required to be supported by any OS. Possibly deprecated.
491: 491:
id: "KSC_EB" id: "KSC_EB"
icon: skip_next
keyCode: "MediaTrackNext" keyCode: "MediaTrackNext"
title: Media Next Song title: Media Next Song
description: Not required to be supported by any OS. Possibly deprecated.
492: 492:
id: "KSC_EC" id: "KSC_EC"
icon: eject
keyCode: "Eject" keyCode: "Eject"
title: Media Eject CD title: Media Eject CD
description: Not required to be supported by any OS. Possibly deprecated. description: MacOS only
493: 493:
id: "KSC_ED" id: "KSC_ED"
icon: volume_up
keyCode: "AudioVolumeUp" keyCode: "AudioVolumeUp"
title: Media Volume Up title: Media Volume Up
description: Not required to be supported by any OS. Possibly deprecated.
494: 494:
id: "KSC_EE" id: "KSC_EE"
icon: volume_down
keyCode: "AudioVolumeDown" keyCode: "AudioVolumeDown"
title: Media Volume Down title: Media Volume Down
description: Not required to be supported by any OS. Possibly deprecated.
495: 495:
id: "KSC_EF" id: "KSC_EF"
icon: volume_off
keyCode: "AudioVolumeMute" keyCode: "AudioVolumeMute"
title: Media Mute title: Media Mute
description: Not required to be supported by any OS. Possibly deprecated.
496: 496:
id: "KSC_F0" id: "KSC_F0"
title: Media www icon: language
description: Not required to be supported by any OS. Possibly deprecated. title: Media Browser
497: 497:
id: "KSC_F1" id: "KSC_F1"
keyCode: "BrowserBack" keyCode: "BrowserBack"
title: Media Back title: Media Browser Back
description: Not required to be supported by any OS. Possibly deprecated.
498: 498:
id: "KSC_F2" id: "KSC_F2"
keyCode: "BrowserForward" keyCode: "BrowserForward"
title: Media Forward title: Media Browser Forward
description: Not required to be supported by any OS. Possibly deprecated.
499: 499:
id: "KSC_F3" id: "KSC_F3"
keyCode: "BrowserStop" keyCode: "BrowserStop"
title: Media Stop title: Media Browser Stop
description: Not required to be supported by any OS. Possibly deprecated. description: Not supported on MacOS
500: 500:
id: "KSC_F4" id: "KSC_F4"
icon: search
keyCode: "BrowserSearch" keyCode: "BrowserSearch"
title: Media Find title: Media Browser Search
description: Not required to be supported by any OS. Possibly deprecated.
501: 501:
id: "KSC_F5" id: "KSC_F5"
title: Media Scroll Up icon: brightness_high
description: Not required to be supported by any OS. Possibly deprecated. title: Media Brightness Up
502: 502:
id: "KSC_F6" id: "KSC_F6"
title: Media Scroll Down icon: brightness_low
description: Not required to be supported by any OS. Possibly deprecated. title: Media Brightness Down
503: 503:
id: "KSC_F7" id: "KSC_F7"
title: Media Edit title: Media Edit
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
504: 504:
id: "KSC_F8" id: "KSC_F8"
icon: bedtime
keyCode: "Sleep" keyCode: "Sleep"
title: Media Sleep title: Media System Sleep
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
505: 505:
id: "KSC_F9" id: "KSC_F9"
icon: routine
keyCode: "WakeUp" keyCode: "WakeUp"
title: Media Coffee title: Media System Wake
description: Not required to be supported by any OS. Possibly deprecated. description: Not supported on Windows
506: 506:
id: "KSC_FA" id: "KSC_FA"
keyCode: "BrowserRefresh" keyCode: "BrowserRefresh"
title: Media Refresh title: Media Browser Refresh
description: Not required to be supported by any OS. Possibly deprecated.
507: 507:
id: "KSC_FB" id: "KSC_FB"
title: Media Calc title: Media Calculator
description: Not required to be supported by any OS. Possibly deprecated. description: Not supported on MacOS
508: 508:
id: "KSC_FC" id: "KSC_FC"
description: Not required to be supported by any OS. description: Not required to be supported by any OS.

View File

@@ -17,7 +17,7 @@
"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", "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", "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!", "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 imporve typing speed when mastered", "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", "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", "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", "Most people find it easier to start their chord library from scratch rather than learning someone else's",
@@ -32,5 +32,7 @@
"You can use Nexus to track words you might want to add to your chord library", "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", "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", "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" "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"
] ]

View File

@@ -96,7 +96,12 @@ export function restoreFromFile(
case "backup": { case "backup": {
const recent = file.history[0]; const recent = file.history[0];
if (!recent) return; if (!recent) return;
if (recent[1].device !== get(serialPort)?.device) { 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"); alert("Backup is incompatible with this device");
throw new Error("Backup is incompatible with this device"); throw new Error("Backup is incompatible with this device");
} }

View File

@@ -3,7 +3,7 @@
import type { KeyInfo } from "$lib/serial/keymap-codes"; import type { KeyInfo } from "$lib/serial/keymap-codes";
import { action as title } from "$lib/title"; import { action as title } from "$lib/title";
import { osLayout } from "$lib/os-layout"; import { osLayout } from "$lib/os-layout";
import LL from "../../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
export let action: number | KeyInfo; export let action: number | KeyInfo;
export let display: "inline-keys" | "keys" = "inline-keys"; export let display: "inline-keys" | "keys" = "inline-keys";
@@ -15,7 +15,8 @@
$: dynamicMapping = info.keyCode && $osLayout.get(info.keyCode); $: dynamicMapping = info.keyCode && $osLayout.get(info.keyCode);
$: tooltip = $: tooltip =
(info.title ?? info.id ?? `0x${info.code.toString(16)}`) + `&lt;${info.id ?? `0x${info.code.toString(16)}`}&gt; ` +
(info.title ?? "") +
(info.variant === "left" (info.variant === "left"
? " (left)" ? " (left)"
: info.variant === "right" : info.variant === "right"

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { KEYMAP_CODES } from "$lib/serial/keymap-codes"; import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
import type { KeyInfo } from "$lib/serial/keymap-codes"; import type { KeyInfo } from "$lib/serial/keymap-codes";
import LL from "../../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
import Action from "$lib/components/Action.svelte"; import Action from "$lib/components/Action.svelte";
export let id: number | KeyInfo; export let id: number | KeyInfo;

View File

@@ -7,7 +7,7 @@
import FlexSearch from "flexsearch"; import FlexSearch from "flexsearch";
import { createEventDispatcher, onMount } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
import ActionListItem from "$lib/components/ActionListItem.svelte"; import ActionListItem from "$lib/components/ActionListItem.svelte";
import LL from "../../../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
import { action } from "$lib/title"; import { action } from "$lib/title";
export let currentAction: number | undefined = undefined; export let currentAction: number | undefined = undefined;

View File

@@ -127,12 +127,11 @@
const clickedGroup = groupParent.children.item(index) as SVGGElement; const clickedGroup = groupParent.children.item(index) as SVGGElement;
const nextAction = get(layout)[get(activeLayer)]?.[keyInfo.id]; const nextAction = get(layout)[get(activeLayer)]?.[keyInfo.id];
const currentAction = get(deviceLayout)[get(activeLayer)]?.[keyInfo.id]; const currentAction = get(deviceLayout)[get(activeLayer)]?.[keyInfo.id];
if (!nextAction || !currentAction) return;
const component = new ActionSelector({ const component = new ActionSelector({
target: document.body, target: document.body,
props: { props: {
currentAction, currentAction,
nextAction: nextAction.isApplied ? undefined : nextAction.action, nextAction: nextAction?.isApplied ? undefined : nextAction?.action,
}, },
}); });
const dialog = document.querySelector("dialog > div") as HTMLDivElement; const dialog = document.querySelector("dialog > div") as HTMLDivElement;

View File

@@ -21,6 +21,10 @@
import("$lib/assets/layouts/one.yml").then( import("$lib/assets/layouts/one.yml").then(
(it) => it.default as VisualLayout, (it) => it.default as VisualLayout,
), ),
TWO: () =>
import("$lib/assets/layouts/one.yml").then(
(it) => it.default as VisualLayout,
),
LITE: () => LITE: () =>
import("$lib/assets/layouts/lite.yml").then( import("$lib/assets/layouts/lite.yml").then(
(it) => it.default as VisualLayout, (it) => it.default as VisualLayout,

View File

@@ -8,7 +8,7 @@
} from "$lib/undo-redo"; } from "$lib/undo-redo";
import { ChangeType, chords } from "$lib/undo-redo"; import { ChangeType, chords } from "$lib/undo-redo";
import ActionString from "$lib/components/ActionString.svelte"; import ActionString from "$lib/components/ActionString.svelte";
import LL from "../../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
import { KEYMAP_IDS } from "$lib/serial/keymap-codes"; import { KEYMAP_IDS } from "$lib/serial/keymap-codes";
export let changes: Change[] = [ export let changes: Change[] = [

View File

@@ -55,3 +55,19 @@ export function deserializeActions(native: bigint): number[] {
return actions; return actions;
} }
/**
* Hashes a chord input the same way as CCOS
*/
export function hashChord(actions: number[]) {
const chord = new Uint8Array(16);
const view = new DataView(chord.buffer);
const serialized = serializeActions(actions);
view.setBigUint64(0, serialized & 0xffff_ffff_ffff_ffffn, true);
view.setBigUint64(8, serialized >> 64n, true);
let hash = 2166136261;
for (let i = 0; i < 16; i++) {
hash = Math.imul(hash ^ view.getUint8(i), 16777619);
}
return hash & 0x3fff_ffff;
}

View File

@@ -12,6 +12,7 @@ import { browser } from "$app/environment";
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([ const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }], ["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
["TWO S3", { usbProductId: 0x0056, usbVendorId: 0x2886 }],
["LITE S2", { usbProductId: 33070, usbVendorId: 12346 }], ["LITE S2", { usbProductId: 33070, usbVendorId: 12346 }],
["LITE M0", { usbProductId: 32796, usbVendorId: 9114 }], ["LITE M0", { usbProductId: 32796, usbVendorId: 9114 }],
["X", { usbProductId: 33163, usbVendorId: 12346 }], ["X", { usbProductId: 33163, usbVendorId: 12346 }],
@@ -19,6 +20,7 @@ const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
const KEY_COUNTS = { const KEY_COUNTS = {
ONE: 90, ONE: 90,
TWO: 90,
LITE: 67, LITE: 67,
X: 256, X: 256,
} as const; } as const;
@@ -87,8 +89,8 @@ export class CharaDevice {
version!: SemVer; version!: SemVer;
company!: "CHARACHORDER"; company!: "CHARACHORDER";
device!: "ONE" | "LITE" | "X"; device!: "ONE" | "TWO" | "LITE" | "X";
chipset!: "M0" | "S2"; chipset!: "M0" | "S2" | "S3";
keyCount!: 90 | 67 | 256; keyCount!: 90 | 67 | 256;
get portInfo() { get portInfo() {
@@ -125,8 +127,8 @@ export class CharaDevice {
); );
const [company, device, chipset] = await this.send(3, "ID"); const [company, device, chipset] = await this.send(3, "ID");
this.company = company as "CHARACHORDER"; this.company = company as "CHARACHORDER";
this.device = device as "ONE" | "LITE" | "X"; this.device = device as "ONE" | "TWO" | "LITE" | "X";
this.chipset = chipset as "M0" | "S2"; this.chipset = chipset as "M0" | "S2" | "S3";
this.keyCount = KEY_COUNTS[this.device]; this.keyCount = KEY_COUNTS[this.device];
} catch (e) { } catch (e) {
alert(e); alert(e);

View File

@@ -72,7 +72,6 @@ export async function charaFileFromUriComponent<T extends CharaFiles>(
.stream() .stream()
.pipeThrough(new DecompressionStream("deflate")); .pipeThrough(new DecompressionStream("deflate"));
const actions = new Uint8Array(await new Response(stream).arrayBuffer()); const actions = new Uint8Array(await new Response(stream).arrayBuffer());
console.log(actions);
file[key] = deserializeActionArray(actions); file[key] = deserializeActionArray(actions);
} }
} }

View File

@@ -1,6 +1,6 @@
import { persistentWritable } from "$lib/storage"; import { persistentWritable } from "$lib/storage";
import { derived } from "svelte/store"; import { derived } from "svelte/store";
import type { Chord } from "$lib/serial/chord"; import { hashChord, type Chord } from "$lib/serial/chord";
import { import {
deviceChords, deviceChords,
deviceLayout, deviceLayout,
@@ -158,3 +158,9 @@ export const chords = derived([overlay, deviceChords], ([overlay, chords]) => {
a.localeCompare(b), a.localeCompare(b),
); );
}); });
export const chordHashes = derived(
chords,
(chords) =>
new Map(chords.map((chord) => [hashChord(chord.actions), chord] as const)),
);

View File

@@ -20,10 +20,10 @@
import "tippy.js/dist/tippy.css"; import "tippy.js/dist/tippy.css";
import tippy from "tippy.js"; import tippy from "tippy.js";
import { theme, userPreferences } from "$lib/preferences.js"; import { theme, userPreferences } from "$lib/preferences.js";
import { LL, setLocale } from "../i18n/i18n-svelte"; import { LL, setLocale } from "$i18n/i18n-svelte";
import { loadLocale } from "../i18n/i18n-util.sync"; import { loadLocale } from "$i18n/i18n-util.sync";
import { detectLocale } from "../i18n/i18n-util"; import { detectLocale } from "$i18n/i18n-util";
import type { Locales } from "../i18n/i18n-types"; import type { Locales } from "$i18n/i18n-types";
import Footer from "./Footer.svelte"; import Footer from "./Footer.svelte";
import { osLayout, runLayoutDetection } from "$lib/os-layout.js"; import { osLayout, runLayoutDetection } from "$lib/os-layout.js";
import PageTransition from "./PageTransition.svelte"; import PageTransition from "./PageTransition.svelte";

View File

@@ -0,0 +1,13 @@
import type { LayoutLoad } from "./$types";
import { browser } from "$app/environment";
import { charaFileFromUriComponent } from "$lib/share/share-url";
export const load = (async ({ url, data, fetch }) => {
const importFile = browser && new URLSearchParams(url.search).get("import");
return {
...data,
importFile: importFile
? await charaFileFromUriComponent(importFile, fetch)
: undefined,
};
}) satisfies LayoutLoad;

View File

@@ -2,5 +2,5 @@ import { redirect } from "@sveltejs/kit";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
export const load = (() => { export const load = (() => {
throw redirect(302, "/config/"); redirect(302, "/config/");
}) satisfies PageLoad; }) satisfies PageLoad;

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { preference } from "$lib/preferences"; import { preference } from "$lib/preferences";
import LL from "../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
import { import {
createChordBackup, createChordBackup,
createLayoutBackup, createLayoutBackup,

View File

@@ -1,5 +1,5 @@
<script> <script>
import LL from "../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
</script> </script>
<dialog open> <dialog open>

View File

@@ -1,6 +1,6 @@
<script> <script>
import { page } from "$app/stores"; import { page } from "$app/stores";
import LL from "../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
$: paths = [ $: paths = [
{ {

View File

@@ -3,7 +3,7 @@
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { slide, fade } from "svelte/transition"; import { slide, fade } from "svelte/transition";
import { preference } from "$lib/preferences"; import { preference } from "$lib/preferences";
import LL from "../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
import { downloadBackup } from "$lib/backup/backup"; import { downloadBackup } from "$lib/backup/backup";
function reboot() { function reboot() {

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import LL from "../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
import { import {
changes, changes,
ChangeType, ChangeType,

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { browser, version } from "$app/environment"; import { browser, version } from "$app/environment";
import { action } from "$lib/title"; import { action } from "$lib/title";
import LL, { setLocale } from "../i18n/i18n-svelte"; import LL, { setLocale } from "$i18n/i18n-svelte";
import { theme } from "$lib/preferences.js"; import { theme } from "$lib/preferences.js";
import type { Locales } from "../i18n/i18n-types"; import type { Locales } from "$i18n/i18n-types";
import { detectLocale, locales } from "../i18n/i18n-util"; import { detectLocale, locales } from "$i18n/i18n-util";
import { loadLocaleAsync } from "../i18n/i18n-util.async"; import { loadLocaleAsync } from "$i18n/i18n-util.async";
import { tick } from "svelte"; import { tick } from "svelte";
import SyncOverlay from "./SyncOverlay.svelte"; import SyncOverlay from "./SyncOverlay.svelte";
import { serialPort } from "$lib/serial/connection"; import { serialPort } from "$lib/serial/connection";

View File

@@ -8,7 +8,7 @@
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { userPreferences } from "$lib/preferences"; import { userPreferences } from "$lib/preferences";
import { action } from "$lib/title"; import { action } from "$lib/title";
import LL from "../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
import ConfigTabs from "./ConfigTabs.svelte"; import ConfigTabs from "./ConfigTabs.svelte";
import EditActions from "./EditActions.svelte"; import EditActions from "./EditActions.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";

View File

@@ -5,7 +5,7 @@
syncStatus, syncStatus,
sync, sync,
} from "$lib/serial/connection"; } from "$lib/serial/connection";
import LL from "../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
import { slide } from "svelte/transition"; import { slide } from "svelte/transition";
</script> </script>

View File

@@ -2,5 +2,5 @@ import { redirect } from "@sveltejs/kit";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
export const load = (() => { export const load = (() => {
throw redirect(302, "/config/layout/"); redirect(302, "/config/layout/");
}) satisfies PageLoad; }) satisfies PageLoad;

View File

@@ -1,5 +1,5 @@
<script> <script>
import LL from "../../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
</script> </script>
{$LL.share.URL_COPIED()} {$LL.share.URL_COPIED()}

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { KEYMAP_CODES } from "$lib/serial/keymap-codes"; import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
import FlexSearch from "flexsearch"; import FlexSearch from "flexsearch";
import LL from "../../../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
import { action } from "$lib/title"; import { action } from "$lib/title";
import { onDestroy, onMount, setContext, tick } from "svelte"; import { onDestroy, onMount, setContext, tick } from "svelte";
import { changes, ChangeType, chords } from "$lib/undo-redo"; import { changes, ChangeType, chords } from "$lib/undo-redo";
@@ -35,7 +35,7 @@
resizeObserver?.disconnect(); resizeObserver?.disconnect();
}); });
let index = new FlexSearch.Index({ tokenize: "full" }); let index = new FlexSearch.Index();
let searchIndex = writable<FlexSearch.Index | undefined>(undefined); let searchIndex = writable<FlexSearch.Index | undefined>(undefined);
$: { $: {
abortIndexing?.(); abortIndexing?.();
@@ -43,22 +43,72 @@
buildIndex($chords, $osLayout).then(searchIndex.set); buildIndex($chords, $osLayout).then(searchIndex.set);
} }
function plainPhrase(phrase: number[], osLayout: Map<string, string>) { function encodeChord(chord: ChordInfo, osLayout: Map<string, string>) {
return phrase const plainPhrase: string[] = [""];
const extraActions: string[] = [];
const extraCodes: string[] = [];
for (const actionCode of chord.phrase ?? []) {
const action = KEYMAP_CODES.get(actionCode);
if (!action) {
extraCodes.push(`0x${actionCode.toString(16)}`);
continue;
}
const osCode = action.keyCode && osLayout.get(action.keyCode);
const token = osCode?.length === 1 ? osCode : action.display || action.id;
if (!token) {
extraCodes.push(`0x${action.code.toString(16)}`);
continue;
}
if (/^\s$/.test(token) && plainPhrase.at(-1) !== "") {
plainPhrase.push("");
} else if (token.length === 1) {
plainPhrase[plainPhrase.length - 1] =
plainPhrase[plainPhrase.length - 1] + token;
} else {
extraActions.push(token);
}
}
if (chord.phrase?.[0] === 298) {
plainPhrase.push("suffix");
}
if (
["ARROW_LT", "ARROW_RT", "ARROW_UP", "ARROW_DN"].some((it) =>
extraActions.includes(it),
)
) {
plainPhrase.push("cursor warp");
}
if (
["CTRL", "ALT", "GUI", "ENTER", "TAB"].some((it) =>
extraActions.includes(it),
)
) {
plainPhrase.push("macro");
}
if (chord.actions[0] !== 0) {
plainPhrase.push("compound");
}
const input = chord.actions
.slice(chord.actions.lastIndexOf(0) + 1)
.map((it) => { .map((it) => {
const info = KEYMAP_CODES.get(it); const info = KEYMAP_CODES.get(it);
if (!info) return ""; if (!info) return `0x${it.toString(16)}`;
const osCode = info.keyCode && osLayout.get(info.keyCode);
const result = osCode?.length === 1 ? osCode : info.id;
return result ?? `0x${it.toString(16)}`;
});
const bestGuess = return [
(info.keyCode && osLayout.get(info.keyCode)) || ...plainPhrase,
info.display || `+${input.join("+")}`,
info.id || ...new Set(extraActions),
""; ...new Set(extraCodes),
].join(" ");
return bestGuess.length === 1 ? bestGuess : "";
})
.filter((it) => !!it)
.join("");
} }
async function buildIndex( async function buildIndex(
@@ -66,7 +116,23 @@
osLayout: Map<string, string>, osLayout: Map<string, string>,
): Promise<FlexSearch.Index> { ): Promise<FlexSearch.Index> {
if (chords.length === 0 || !browser) return index; if (chords.length === 0 || !browser) return index;
index = new FlexSearch.Index({ tokenize: "full" }); index = new FlexSearch.Index({
tokenize: "full",
encode(phrase: string) {
return phrase.split(/\s+/).flatMap((it) => {
if (/^[A-Z_]+$/.test(it)) {
return it;
}
if (it.startsWith("+")) {
return it
.slice(1)
.split("+")
.map((it) => `+${it}`);
}
return it.toLowerCase();
});
},
});
let abort = false; let abort = false;
abortIndexing = () => { abortIndexing = () => {
abort = true; abort = true;
@@ -78,7 +144,7 @@
progress = i; progress = i;
if ("phrase" in chord) { if ("phrase" in chord) {
await index.addAsync(i, plainPhrase(chord.phrase, osLayout)); await index.addAsync(i, encodeChord(chord, osLayout));
} }
} }
return index; return index;

View File

@@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { ChordInfo } from "$lib/undo-redo"; import type { ChordInfo } from "$lib/undo-redo";
import { changes, ChangeType } from "$lib/undo-redo"; import { changes, chordHashes, ChangeType } from "$lib/undo-redo";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import LL from "../../../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
import ActionString from "$lib/components/ActionString.svelte"; import ActionString from "$lib/components/ActionString.svelte";
import { selectAction } from "./action-selector"; import { selectAction } from "./action-selector";
import { serialPort } from "$lib/serial/connection"; import { serialPort } from "$lib/serial/connection";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { inputToAction } from "./input-converter"; import { inputToAction } from "./input-converter";
import { hashChord } from "$lib/serial/chord";
export let chord: ChordInfo | undefined = undefined; export let chord: ChordInfo | undefined = undefined;
@@ -21,14 +22,15 @@
} }
function makeChordInput(...actions: number[]) { function makeChordInput(...actions: number[]) {
const compound = compoundIndices ?? []; const compound = compoundInputs[0]
? hashChord(compoundInputs[0].actions)
: 0;
return [ return [
...compound,
...Array.from( ...Array.from(
{ {
length: 12 - (compound.length + actions.length + 1), length: 12 - actions.length,
}, },
() => 0, (_, i) => (compound >> (i * 10)) & 0x3ff,
), ),
...actions.toSorted(compare), ...actions.toSorted(compare),
]; ];
@@ -73,7 +75,6 @@
function addSpecial(event: MouseEvent) { function addSpecial(event: MouseEvent) {
selectAction(event, (action) => { selectAction(event, (action) => {
changes.update((changes) => { changes.update((changes) => {
console.log(compoundIndices, chordActions, action);
changes.push({ changes.push({
type: ChangeType.Chord, type: ChangeType.Chord,
id: chord!.id, id: chord!.id,
@@ -85,10 +86,30 @@
}); });
} }
function* resolveCompound(chord?: ChordInfo) {
if (!chord) return;
let current = chord;
for (let i = 0; i < 10; i++) {
if (current.actions[3] !== 0) return;
const compound = current.actions
.slice(0, 3)
.reduce((a, b, i) => a | (b << (i * 10)));
if (compound === 0) return;
const next = $chordHashes.get(compound);
if (!next) {
return null;
}
current = next;
yield next;
}
return;
}
$: chordActions = chord?.actions $: chordActions = chord?.actions
.slice(chord.actions.lastIndexOf(0) + 1) .slice(chord.actions.lastIndexOf(0) + 1)
.toSorted(compare); .toSorted(compare);
$: compoundIndices = chord?.actions.slice(0, chord.actions.indexOf(0)); $: compoundInputs = [...resolveCompound(chord)].reverse();
</script> </script>
<button <button
@@ -110,12 +131,15 @@
<span>{$LL.configure.chords.NEW_CHORD()}</span> <span>{$LL.configure.chords.NEW_CHORD()}</span>
{/if} {/if}
{#if !editing} {#if !editing}
{#each compoundIndices ?? [] as index} {#each compoundInputs as compound}
<sub>{index}</sub> <sub
{/each} ><ActionString
{#if compoundIndices?.length} display="keys"
actions={compound.actions.slice(compound.actions.lastIndexOf(0) + 1)}
></ActionString>
</sub>
<span>&rarr;</span> <span>&rarr;</span>
{/if} {/each}
{/if} {/if}
<ActionString <ActionString
display="keys" display="keys"

View File

@@ -7,11 +7,16 @@
import { keymap } from "@codemirror/view"; import { keymap } from "@codemirror/view";
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { tags } from "@lezer/highlight"; import { tags } from "@lezer/highlight";
import LL from "../../i18n/i18n-svelte"; import LL from "$i18n/i18n-svelte";
import type { CompletionContext } from "@codemirror/autocomplete"; import type { CompletionContext, Completion } from "@codemirror/autocomplete";
import { syntaxTree } from "@codemirror/language";
import { serialPort } from "$lib/serial/connection"; import { serialPort } from "$lib/serial/connection";
import type { CharaDevice } from "$lib/serial/device";
import examplePlugin from "./example-plugin.js?raw"; import examplePlugin from "./example-plugin.js?raw";
import {
charaMethods,
type ChannelCharaEventData,
type ChannelResponseEventData,
} from "./plugin-types";
let theme = EditorView.baseTheme({ let theme = EditorView.baseTheme({
".cm-editor .cm-content": { ".cm-editor .cm-content": {
@@ -40,6 +45,18 @@
background: "transparent !important", background: "transparent !important",
backdropFilter: "invert(0.3)", backdropFilter: "invert(0.3)",
}, },
".cm-tooltip": {
backgroundColor: "var(--md-sys-color-background) !important",
color: "var(--md-sys-color-on-background)",
borderColor: "var(--md-sys-color-outline)",
},
".cm-tooltip-autocomplete ul li[aria-selected]": {
backgroundColor: "var(--md-sys-color-primary) !important",
color: "var(--md-sys-color-on-primary) !important",
},
".cm-completionIcon.cm-completionIcon-keyword::after": {
content: "'🗝'",
},
}); });
const highlightStyle = HighlightStyle.define( const highlightStyle = HighlightStyle.define(
[ [
@@ -56,11 +73,71 @@
all: { fontFamily: '"Noto Sans Mono", monospace', fontSize: "14px" }, all: { fontFamily: '"Noto Sans Mono", monospace', fontSize: "14px" },
}, },
); );
const globalsCompletion: Completion[] = [
{ label: "Chara", type: "class", boost: 90 },
{ label: "Actions", type: "class", boost: 90 },
];
const actionsCompletion: Completion[] = Array.from(
KEYMAP_CODES,
([id, info]) => {
const isValidIdentifier =
info.id && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(info.id);
return {
label: info.id
? isValidIdentifier
? info.id
: `["${info.id}"]`
: info.id!,
displayLabel: info.id,
detail: [info.title, `(0x${id.toString(16)})`, info.description]
.filter((it) => !!it)
.join(" "),
section: info.category,
boost: isValidIdentifier ? Math.min(info.id?.length ?? 0, 10) + 50 : 40,
type: "property",
};
},
).filter((it) => it.label !== undefined);
const completion = javascriptLanguage.data.of({ const completion = javascriptLanguage.data.of({
autocomplete: function completeGlobals(context: CompletionContext) { autocomplete: function completeGlobals(context: CompletionContext) {
if (context.matchBefore(/Chara\./)) { let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
// TODO if (nodeBefore.name === "VariableName") {
return {
from: nodeBefore.from,
options: globalsCompletion,
};
} else if (nodeBefore.name === "Script") {
return {
from: context.pos,
options: globalsCompletion,
};
} else if (
(nodeBefore.name === "PropertyName" || nodeBefore.name === ".") &&
nodeBefore.parent?.name === "MemberExpression" &&
nodeBefore.parent.firstChild
) {
const variable = nodeBefore.parent.firstChild;
const variableName = context.state.sliceDoc(variable.from, variable.to);
if (variableName === "Actions") {
return {
from:
nodeBefore.name === "PropertyName"
? nodeBefore.from
: nodeBefore.to,
options: actionsCompletion,
};
}
let parent = nodeBefore.prevSibling;
while (parent !== null && parent?.name !== "VariableName") {
parent = parent.prevSibling;
}
if (parent) {
}
} }
return null;
}, },
}); });
@@ -78,22 +155,6 @@
doc: examplePlugin, doc: examplePlugin,
}); });
}); });
const charaMethods = [
"reboot",
"bootloader",
"getRamBytesAvailable",
"getSetting",
"setSetting",
"getLayoutKey",
"setLayoutKey",
"deleteChord",
"setChord",
"getChordPhrase",
"getChordCount",
"getChord",
"send",
] satisfies Array<keyof CharaDevice>;
$: channels = $serialPort $: channels = $serialPort
? ({ ? ({
getVersion: async (..._args: unknown[]) => $serialPort.version, getVersion: async (..._args: unknown[]) => $serialPort.version,
@@ -122,7 +183,10 @@
const [channel, params] = event.data; const [channel, params] = event.data;
const response = channels[channel as keyof typeof channels](...params); const response = channels[channel as keyof typeof channels](...params);
frame.contentWindow!.postMessage({ response: await response }, "*"); frame.contentWindow!.postMessage(
{ response: await response } satisfies ChannelResponseEventData,
"*",
);
} }
function runPlugin() { function runPlugin() {
@@ -131,7 +195,7 @@
actionCodes: KEYMAP_CODES, actionCodes: KEYMAP_CODES,
script: editorView.state.doc.toString(), script: editorView.state.doc.toString(),
charaChannels: Object.keys(channels), charaChannels: Object.keys(channels),
}, } satisfies ChannelCharaEventData,
"*", "*",
); );
} }

View File

@@ -0,0 +1,30 @@
import type { CharaDevice } from "$lib/serial/device";
import type { KeyInfo } from "$lib/serial/keymap-codes";
export const charaMethods = [
"reboot",
"bootloader",
"getRamBytesAvailable",
"getSetting",
"setSetting",
"getLayoutKey",
"setLayoutKey",
"deleteChord",
"setChord",
"getChordPhrase",
"getChordCount",
"getChord",
"send",
] as const satisfies Array<keyof CharaDevice>;
export interface ChannelResponseEventData {
response: unknown;
}
export interface ChannelCharaEventData {
charaChannels: string[];
script: string;
actionCodes: Map<number, KeyInfo>;
}
export type ChannelEventData = ChannelResponseEventData | ChannelCharaEventData;

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { LL } from "../../i18n/i18n-svelte"; import { LL } from "$i18n/i18n-svelte";
</script> </script>
<h1>{$LL.update.TITLE()}</h1> <h1>{$LL.update.TITLE()}</h1>

View File

@@ -1,16 +1,2 @@
import type { LayoutLoad } from "./$types";
import { browser } from "$app/environment";
import { charaFileFromUriComponent } from "$lib/share/share-url";
export const prerender = true; export const prerender = true;
export const trailingSlash = "always"; export const trailingSlash = "always";
export const load = (async ({ url, data, fetch }) => {
const importFile = browser && new URLSearchParams(url.search).get("import");
return {
...data,
importFile: importFile
? await charaFileFromUriComponent(importFile, fetch)
: undefined,
};
}) satisfies LayoutLoad;

View File

@@ -1,15 +1,19 @@
<script> <script lang="ts">
// @ts-nocheck import type { ChannelEventData } from "../(app)/plugin/plugin-types";
let ongoingRequest;
let resolveRequest; let ongoingRequest: Promise<unknown> | undefined = undefined;
let source; let resolveRequest: ((data: unknown) => void) | undefined = undefined;
async function post(channel, args) { let source: MessageEventSource | undefined = undefined;
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
async function post(channel: string, args: unknown[]) {
while (ongoingRequest) { while (ongoingRequest) {
await ongoingRequest; await ongoingRequest;
} }
ongoingRequest = new Promise((resolve) => { ongoingRequest = new Promise((resolve) => {
resolveRequest = resolve; resolveRequest = resolve;
source.postMessage([channel, args], "*"); source?.postMessage([channel, args], { targetOrigin: "*" });
}); });
ongoingRequest.then(() => { ongoingRequest.then(() => {
ongoingRequest = undefined; ongoingRequest = undefined;
@@ -17,13 +21,13 @@
return ongoingRequest; return ongoingRequest;
} }
window.addEventListener("message", (event) => { function onMessage(event: MessageEvent<ChannelEventData>) {
if ("response" in event.data) { if ("response" in event.data) {
resolveRequest(event.data.response); resolveRequest?.(event.data.response);
} else { } else {
source = event.source; source = event.source ?? undefined;
var Action = event.data.actionCodes; const Action = event.data.actionCodes;
Object.assign( Object.assign(
Action, Action,
Object.fromEntries( Object.fromEntries(
@@ -33,12 +37,20 @@
), ),
); );
var Chara = {}; const Chara = Object.fromEntries(
for (const fn of event.data.charaChannels) { event.data.charaChannels.map((name) => [
Chara[fn] = (...args) => post(fn, args); name,
} (...args: unknown[]) => post(name, args),
]),
);
eval(`(async function(){${event.data.script}})()`); AsyncFunction(
"Action",
"Chara",
'"use strict"\n' + event.data.script,
)(Action, Chara);
} }
}); }
</script> </script>
<svelte:window on:message={onMessage} />

60
static/sandbox/index.html Normal file
View File

@@ -0,0 +1,60 @@
<script>
/** @type {Promise<unknown> | undefined} */
let ongoingRequest = undefined;
/** @type {(data: unknown) => void | undefined} */
let resolveRequest = undefined;
/** @type {MessageEventSource | undefined} */
let source = undefined;
/**
* @param {string} channel
* @param {unknown} args
* @returns {Promise<unknown>}
*/
async function post(channel, args) {
while (ongoingRequest) {
await ongoingRequest;
}
ongoingRequest = new Promise((resolve) => {
resolveRequest = resolve;
source?.postMessage([channel, args], { targetOrigin: "*" });
});
ongoingRequest.then(() => {
ongoingRequest = undefined;
});
return ongoingRequest;
}
/**
* @param {MessageEvent<import('../../src/routes/plugin/plugin-types').ChannelEventData>} event
*/
function onMessage(event) {
if ("response" in event.data) {
resolveRequest?.(event.data.response);
} else {
source = event.source ?? undefined;
const Action = event.data.actionCodes;
Object.assign(
Action,
Object.fromEntries(
Object.values(event.data.actionCodes)
.filter((it) => !!it.id)
.map((it) => [it.id, it]),
),
);
new Function("Action", "Chara", event.data.script)(
Action,
Object.fromEntries(
event.data.charaChannels.map((name) => [
name,
(...args) => post(name, args),
]),
),
);
}
}
window.addEventListener("message", onMessage);
</script>

View File

@@ -1,5 +1,5 @@
import adapter from "@sveltejs/adapter-static"; import adapter from "@sveltejs/adapter-static";
import preprocess from "svelte-preprocess"; import { sveltePreprocess } from "svelte-preprocess";
import autoprefixer from "autoprefixer"; import autoprefixer from "autoprefixer";
import { readFile } from "fs/promises"; import { readFile } from "fs/promises";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
@@ -13,9 +13,12 @@ const { version } = JSON.parse(
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
preprocess: [preprocess({ postcss: { plugins: autoprefixer() } })], preprocess: [sveltePreprocess({ postcss: { plugins: autoprefixer() } })],
kit: { kit: {
adapter: adapter({ fallback: "404.html" }), adapter: adapter({ fallback: "404.html" }),
alias: {
$i18n: "./src/i18n",
},
version: { version: {
name: version, name: version,
}, },

View File

@@ -20,7 +20,7 @@ process.env["VITE_HOMEPAGE_URL"] = repository.url.replace(/\.git$/, "");
process.env["VITE_DOCS_URL"] = homepage; process.env["VITE_DOCS_URL"] = homepage;
process.env["VITE_BUGS_URL"] = bugs.url; process.env["VITE_BUGS_URL"] = bugs.url;
process.env["VITE_LEARN_URL"] = "https://www.iq-eq.io/"; process.env["VITE_LEARN_URL"] = "https://www.iq-eq.io/";
process.env["VITE_LATEST_FIRMWARE"] = "1.1.3"; process.env["VITE_LATEST_FIRMWARE"] = "1.1.4";
process.env["VITE_STORE_URL"] = "https://www.charachorder.com/"; process.env["VITE_STORE_URL"] = "https://www.charachorder.com/";
export default defineConfig({ export default defineConfig({
@@ -49,9 +49,10 @@ export default defineConfig({
workbox: { workbox: {
// https://vite-pwa-org.netlify.app/frameworks/sveltekit.html#globpatterns // https://vite-pwa-org.netlify.app/frameworks/sveltekit.html#globpatterns
globPatterns: [ globPatterns: [
"client/**/*.{js,map,css,woff2,csv,png,svg}", "client/**/*.{js,map,css,ico,woff2,csv,png,webp,svg,webmanifest}",
"prerendered/**/*.html", "prerendered/**/*.html",
], ],
ignoreURLParametersMatching: [/^import$/],
}, },
manifest: { manifest: {
name: "CharaChorder Device Manager", name: "CharaChorder Device Manager",