12 Commits

Author SHA1 Message Date
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
61 changed files with 7590 additions and 11910 deletions

View File

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

117
flake.nix
View File

@@ -4,56 +4,75 @@
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)];
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_18
rust-bin
fontMin
])
++ (with tauriPkgs; [
curl
wget
pkg-config
outputs =
{
self,
nixpkgs,
flake-utils,
rust-overlay,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
overlays = [ (import rust-overlay) ];
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
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
'';
};
});
];
packages =
(with pkgs; [
nodejs_18
nodePackages.pnpm
rust-bin
fontMin
])
++ (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",
stat_minus_2: "e69c",
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",
"version": "1.5.0",
"version": "1.5.2",
"license": "AGPL-3.0-or-later",
"private": true,
"engines": {
"node": ">=18.16",
"pnpm": ">=8.6"
},
"repository": {
"type": "git",
"url": "https://github.com/CharaChorder/DeviceManager.git"
@@ -30,52 +34,55 @@
"typesafe-i18n": "typesafe-i18n"
},
"devDependencies": {
"@codemirror/autocomplete": "^6.15.0",
"@codemirror/commands": "^6.3.3",
"@codemirror/autocomplete": "^6.17.0",
"@codemirror/commands": "^6.6.0",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/language": "^6.10.1",
"@codemirror/language": "^6.10.2",
"@codemirror/state": "^6.4.1",
"@fontsource-variable/material-symbols-rounded": "^5.0.27",
"@fontsource-variable/noto-sans-mono": "^5.0.19",
"@material/material-color-utilities": "^0.2.7",
"@codemirror/view": "^6.28.4",
"@fontsource-variable/material-symbols-rounded": "^5.0.34",
"@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",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.30.4",
"@sveltejs/vite-plugin-svelte": "^2.5.3",
"@tauri-apps/api": "^1.5.3",
"@tauri-apps/cli": "^1.5.11",
"@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/kit": "^2.5.18",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tauri-apps/api": "^1.6.0",
"@tauri-apps/cli": "^1.6.0",
"@types/dom-view-transitions": "^1.0.4",
"@types/flexsearch": "^0.7.6",
"@types/w3c-web-serial": "^1.0.6",
"@types/w3c-web-usb": "^1.0.10",
"@vite-pwa/sveltekit": "^0.2.10",
"@vite-pwa/sveltekit": "^0.6.0",
"autoprefixer": "^10.4.19",
"codemirror": "^6.0.1",
"cypress": "^13.7.2",
"cypress": "^13.13.0",
"flexsearch": "^0.7.43",
"fontkit": "^2.0.2",
"glob": "^10.3.12",
"jsdom": "^22.1.0",
"glob": "^10.4.3",
"jsdom": "^24.1.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"sass": "^1.74.1",
"stylelint": "^15.11.0",
"stylelint-config-clean-order": "^5.4.2",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.5",
"sass": "^1.77.6",
"stylelint": "^16.6.1",
"stylelint-config-clean-order": "^6.1.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-prettier-scss": "^1.0.0",
"stylelint-config-recommended-scss": "^13.1.0",
"stylelint-config-standard-scss": "^11.1.0",
"svelte": "^4.2.12",
"svelte-check": "^3.6.9",
"svelte-preprocess": "^5.1.3",
"stylelint-config-recommended-scss": "^14.0.0",
"stylelint-config-standard-scss": "^13.1.0",
"svelte": "^4.2.18",
"svelte-check": "^3.8.4",
"svelte-preprocess": "^6.0.1",
"tippy.js": "^6.3.7",
"typesafe-i18n": "^5.26.2",
"typescript": "^5.4.4",
"vite": "^4.5.3",
"typescript": "^5.5.3",
"vite": "^5.3.3",
"vite-plugin-mkcert": "^1.17.5",
"vite-plugin-pwa": "^0.17.5",
"vitest": "^0.34.6"
"vite-plugin-pwa": "^0.20.0",
"vitest": "^1.6.0",
"workbox-window": "^7.1.0"
},
"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]
name = "app"
version = "1.5.0"
version = "1.5.2"
description = "A Tauri App"
authors = ["Thea Schöbl <dev@theaninova.de>"]
license = "AGPL-3"

View File

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

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
@@ -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,6 +96,9 @@ 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

View File

@@ -422,8 +422,8 @@ actions:
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"
@@ -944,99 +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"
keyCode: "BrowserBack"
title: Media Back
description: Not required to be supported by any OS. Possibly deprecated.
title: Media Browser Back
498:
id: "KSC_F2"
keyCode: "BrowserForward"
title: Media Forward
description: Not required to be supported by any OS. Possibly deprecated.
title: Media Browser Forward
499:
id: "KSC_F3"
keyCode: "BrowserStop"
title: Media Stop
description: Not required to be supported by any OS. Possibly deprecated.
title: Media Browser Stop
description: Not supported on MacOS
500:
id: "KSC_F4"
icon: search
keyCode: "BrowserSearch"
title: Media Find
description: Not required to be supported by any OS. Possibly deprecated.
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"
icon: bedtime
keyCode: "Sleep"
title: Media Sleep
title: Media System Sleep
description: Not required to be supported by any OS. Possibly deprecated.
505:
id: "KSC_F9"
icon: routine
keyCode: "WakeUp"
title: Media Coffee
description: Not required to be supported by any OS. Possibly deprecated.
title: Media System Wake
description: Not supported on Windows
506:
id: "KSC_FA"
keyCode: "BrowserRefresh"
title: Media Refresh
description: Not required to be supported by any OS. Possibly deprecated.
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.

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",
"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 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",
"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",
@@ -32,5 +32,7 @@
"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"
"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": {
const recent = file.history[0];
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");
throw new Error("Backup is incompatible with this device");
}

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { KEYMAP_CODES } 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";
export let id: number | KeyInfo;

View File

@@ -7,7 +7,7 @@
import FlexSearch from "flexsearch";
import { createEventDispatcher, onMount } from "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";
export let currentAction: number | undefined = undefined;

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
} from "$lib/undo-redo";
import { ChangeType, chords } from "$lib/undo-redo";
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";
export let changes: Change[] = [

View File

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

View File

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

View File

@@ -20,10 +20,10 @@
import "tippy.js/dist/tippy.css";
import tippy from "tippy.js";
import { theme, userPreferences } from "$lib/preferences.js";
import { LL, setLocale } from "../i18n/i18n-svelte";
import { loadLocale } from "../i18n/i18n-util.sync";
import { detectLocale } from "../i18n/i18n-util";
import type { Locales } from "../i18n/i18n-types";
import { LL, setLocale } from "$i18n/i18n-svelte";
import { loadLocale } from "$i18n/i18n-util.sync";
import { detectLocale } from "$i18n/i18n-util";
import type { Locales } from "$i18n/i18n-types";
import Footer from "./Footer.svelte";
import { osLayout, runLayoutDetection } from "$lib/os-layout.js";
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";
export const load = (() => {
throw redirect(302, "/config/");
redirect(302, "/config/");
}) satisfies PageLoad;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { browser, version } from "$app/environment";
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 type { Locales } from "../i18n/i18n-types";
import { detectLocale, locales } from "../i18n/i18n-util";
import { loadLocaleAsync } from "../i18n/i18n-util.async";
import type { Locales } from "$i18n/i18n-types";
import { detectLocale, locales } from "$i18n/i18n-util";
import { loadLocaleAsync } from "$i18n/i18n-util.async";
import { tick } from "svelte";
import SyncOverlay from "./SyncOverlay.svelte";
import { serialPort } from "$lib/serial/connection";

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
import FlexSearch from "flexsearch";
import LL from "../../../i18n/i18n-svelte";
import LL from "$i18n/i18n-svelte";
import { action } from "$lib/title";
import { onDestroy, onMount, setContext, tick } from "svelte";
import { changes, ChangeType, chords } from "$lib/undo-redo";
@@ -35,7 +35,7 @@
resizeObserver?.disconnect();
});
let index = new FlexSearch.Index({ tokenize: "full" });
let index = new FlexSearch.Index();
let searchIndex = writable<FlexSearch.Index | undefined>(undefined);
$: {
abortIndexing?.();
@@ -43,22 +43,72 @@
buildIndex($chords, $osLayout).then(searchIndex.set);
}
function plainPhrase(phrase: number[], osLayout: Map<string, string>) {
return phrase
function encodeChord(chord: ChordInfo, osLayout: Map<string, string>) {
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) => {
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 =
(info.keyCode && osLayout.get(info.keyCode)) ||
info.display ||
info.id ||
"";
return bestGuess.length === 1 ? bestGuess : "";
})
.filter((it) => !!it)
.join("");
return [
...plainPhrase,
`+${input.join("+")}`,
...new Set(extraActions),
...new Set(extraCodes),
].join(" ");
}
async function buildIndex(
@@ -66,7 +116,23 @@
osLayout: Map<string, string>,
): Promise<FlexSearch.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;
abortIndexing = () => {
abort = true;
@@ -78,7 +144,8 @@
progress = i;
if ("phrase" in chord) {
await index.addAsync(i, plainPhrase(chord.phrase, osLayout));
console.log(encodeChord(chord, osLayout));
await index.addAsync(i, encodeChord(chord, osLayout));
}
}
return index;

View File

@@ -2,7 +2,7 @@
import type { ChordInfo } from "$lib/undo-redo";
import { changes, ChangeType } from "$lib/undo-redo";
import { createEventDispatcher } from "svelte";
import LL from "../../../i18n/i18n-svelte";
import LL from "$i18n/i18n-svelte";
import ActionString from "$lib/components/ActionString.svelte";
import { selectAction } from "./action-selector";
import { serialPort } from "$lib/serial/connection";

View File

@@ -7,11 +7,16 @@
import { keymap } from "@codemirror/view";
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { tags } from "@lezer/highlight";
import LL from "../../i18n/i18n-svelte";
import type { CompletionContext } from "@codemirror/autocomplete";
import LL from "$i18n/i18n-svelte";
import type { CompletionContext, Completion } from "@codemirror/autocomplete";
import { syntaxTree } from "@codemirror/language";
import { serialPort } from "$lib/serial/connection";
import type { CharaDevice } from "$lib/serial/device";
import examplePlugin from "./example-plugin.js?raw";
import {
charaMethods,
type ChannelCharaEventData,
type ChannelResponseEventData,
} from "./plugin-types";
let theme = EditorView.baseTheme({
".cm-editor .cm-content": {
@@ -40,6 +45,18 @@
background: "transparent !important",
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(
[
@@ -56,11 +73,74 @@
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({
autocomplete: function completeGlobals(context: CompletionContext) {
if (context.matchBefore(/Chara\./)) {
// TODO
let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
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) {
}
}
console.log(nodeBefore);
console.log(context);
return null;
},
});
@@ -78,22 +158,6 @@
doc: examplePlugin,
});
});
const charaMethods = [
"reboot",
"bootloader",
"getRamBytesAvailable",
"getSetting",
"setSetting",
"getLayoutKey",
"setLayoutKey",
"deleteChord",
"setChord",
"getChordPhrase",
"getChordCount",
"getChord",
"send",
] satisfies Array<keyof CharaDevice>;
$: channels = $serialPort
? ({
getVersion: async (..._args: unknown[]) => $serialPort.version,
@@ -122,7 +186,10 @@
const [channel, params] = event.data;
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() {
@@ -131,7 +198,7 @@
actionCodes: KEYMAP_CODES,
script: editorView.state.doc.toString(),
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">
import { LL } from "../../i18n/i18n-svelte";
import { LL } from "$i18n/i18n-svelte";
</script>
<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 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,17 @@
<script>
// @ts-nocheck
let ongoingRequest;
let resolveRequest;
let source;
async function post(channel, args) {
<script lang="ts">
import type { ChannelEventData } from "../(app)/plugin/plugin-types";
let ongoingRequest: Promise<unknown> | undefined = undefined;
let resolveRequest: ((data: unknown) => void) | undefined = undefined;
let source: MessageEventSource | undefined = undefined;
async function post(channel: string, args: unknown[]) {
while (ongoingRequest) {
await ongoingRequest;
}
ongoingRequest = new Promise((resolve) => {
resolveRequest = resolve;
source.postMessage([channel, args], "*");
source?.postMessage([channel, args], { targetOrigin: "*" });
});
ongoingRequest.then(() => {
ongoingRequest = undefined;
@@ -17,13 +19,13 @@
return ongoingRequest;
}
window.addEventListener("message", (event) => {
function onMessage(event: MessageEvent<ChannelEventData>) {
if ("response" in event.data) {
resolveRequest(event.data.response);
resolveRequest?.(event.data.response);
} else {
source = event.source;
source = event.source ?? undefined;
var Action = event.data.actionCodes;
const Action = event.data.actionCodes;
Object.assign(
Action,
Object.fromEntries(
@@ -33,12 +35,17 @@
),
);
var Chara = {};
for (const fn of event.data.charaChannels) {
Chara[fn] = (...args) => post(fn, args);
}
eval(`(async function(){${event.data.script}})()`);
new Function("Action", "Chara", event.data.script)(
Action,
Object.fromEntries(
event.data.charaChannels.map((name) => [
name,
(...args: unknown[]) => post(name, args),
]),
),
);
}
});
}
</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 preprocess from "svelte-preprocess";
import { sveltePreprocess } from "svelte-preprocess";
import autoprefixer from "autoprefixer";
import { readFile } from "fs/promises";
import { fileURLToPath } from "url";
@@ -13,9 +13,12 @@ const { version } = JSON.parse(
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: [preprocess({ postcss: { plugins: autoprefixer() } })],
preprocess: [sveltePreprocess({ postcss: { plugins: autoprefixer() } })],
kit: {
adapter: adapter({ fallback: "404.html" }),
alias: {
$i18n: "./src/i18n",
},
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_BUGS_URL"] = bugs.url;
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/";
export default defineConfig({
@@ -49,9 +49,10 @@ export default defineConfig({
workbox: {
// https://vite-pwa-org.netlify.app/frameworks/sveltekit.html#globpatterns
globPatterns: [
"client/**/*.{js,map,css,woff2,csv,png,svg}",
"client/**/*.{js,map,css,ico,woff2,csv,png,webp,svg,webmanifest}",
"prerendered/**/*.html",
],
ignoreURLParametersMatching: [/^import$/],
},
manifest: {
name: "CharaChorder Device Manager",