opencharm 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,7 @@ npx opencharm
20
20
  | `opencharm pair <code>` | Pair the charm showing that code; you choose its PIN (`--name <name>`) |
21
21
  | `opencharm status` | What's running (charmd, each connected charm's build, the starter commit of the workspace; spec 015), then the paired charms and whether they are connected, locked or blocked |
22
22
  | `opencharm lock` / `unlock` / `revoke <charm>` | Lock it now; clear a block and unlock; forget it |
23
+ | `opencharm replies on\|off` | The desktop charm speaks its replies, or shows them as text, from the next one; a charm on a board always speaks. To keep it, set `"speakReplies": false` in `opencharm.json` |
23
24
  | `opencharm look` | Show the charm's look, or change it on every unlocked charm until charmd restarts (`--colour <colour>`, `--greeting <text>`, `--motion full\|calm`); to keep it, set `charm` in `opencharm.json` |
24
25
  | `opencharm dev face <charm> <state> [text]` / `dev say <charm> <text>` / `dev ask <charm> <question>` | Show a face, speak, or ask a question (hold = yes, press = no) on a connected charm (testing) |
25
26
  | `opencharm voice` / `voice install` | The local voice's models (Parakeet listening, Supertonic speaking): whether they're here, and download them now (charmd does it on its first start otherwise) |
package/dist/main.mjs CHANGED
@@ -497,6 +497,11 @@ const unlock = z.object({
497
497
  op: z.literal("unlock"),
498
498
  pin: z.string().regex(/^\d{4,12}$/)
499
499
  });
500
+ const typed = z.object({
501
+ type: z.literal("charm"),
502
+ op: z.literal("text"),
503
+ text: z.string().max(2e3).refine((text) => text.trim().length > 0, { message: "empty" })
504
+ });
500
505
  const answer = z.object({
501
506
  type: z.literal("charm"),
502
507
  op: z.literal("answer"),
@@ -507,7 +512,8 @@ const serverHello = z.object({
507
512
  type: z.literal("hello"),
508
513
  transport: z.literal("websocket"),
509
514
  session_id: z.string().min(1).max(128),
510
- audio_params: audioParams
515
+ audio_params: audioParams,
516
+ features: z.object({ text: z.boolean().optional() }).optional()
511
517
  });
512
518
  const stt = z.object({
513
519
  session_id: sessionId,
@@ -593,7 +599,8 @@ const CLIENT_SCHEMAS = {
593
599
  listen,
594
600
  abort,
595
601
  "charm:unlock": unlock,
596
- "charm:answer": answer
602
+ "charm:answer": answer,
603
+ "charm:text": typed
597
604
  };
598
605
  const SERVER_SCHEMAS = {
599
606
  hello: serverHello,
@@ -828,6 +835,7 @@ const schema = z.object({
828
835
  agent: agentSchema.default({ adapter: "fake" }),
829
836
  turnTimeoutSeconds: z.number().int().min(5).max(600).default(60),
830
837
  logTranscripts: z.boolean().default(false),
838
+ speakReplies: z.boolean().default(true),
831
839
  charm: charmSchema.prefault({})
832
840
  }).strict();
833
841
  function defaultAdminSocket(statePath) {
@@ -961,7 +969,7 @@ function sendAdmin(socketPath, request) {
961
969
  }
962
970
  //#endregion
963
971
  //#region package.json
964
- var version = "0.4.0";
972
+ var version = "0.6.0";
965
973
  function channelToCube(value) {
966
974
  if (value < 48) return 0;
967
975
  if (value < 115) return 1;
@@ -1225,6 +1233,19 @@ async function runAdminCommand(ctx, command, args, deps) {
1225
1233
  await look(ctx, args, deps);
1226
1234
  return;
1227
1235
  }
1236
+ if (command === "replies") {
1237
+ const choice = firstPositional(args);
1238
+ if (choice !== "on" && choice !== "off") {
1239
+ fail$2(ctx, "Usage: opencharm replies on|off (spoken, or text on the desktop charm)");
1240
+ return;
1241
+ }
1242
+ await deps.send(deps.socket, {
1243
+ cmd: "replies",
1244
+ speak: choice === "on"
1245
+ });
1246
+ ctx.out.write(choice === "on" ? "Replies are spoken, from the next one.\n" : "Replies show as text on the desktop charm, from the next one (a charm on a board still speaks).\n");
1247
+ return;
1248
+ }
1228
1249
  if (command === "status") {
1229
1250
  const answer = statusSchema.safeParse(await deps.send(deps.socket, { cmd: "status" }));
1230
1251
  if (!answer.success) {
@@ -1452,6 +1473,7 @@ function runHelp(ctx) {
1452
1473
  opencharm lock <charm> lock a charm now
1453
1474
  opencharm unlock <charm> clear a block and unlock
1454
1475
  opencharm revoke <charm> forget a charm (it must pair again)
1476
+ opencharm replies on|off the desktop charm speaks its replies, or shows them as text
1455
1477
  opencharm look show or change the charm's look until charmd restarts [--colour <c>] [--greeting <text>] [--motion full|calm]
1456
1478
  opencharm dev face <charm> <state> [text] show a face (testing)
1457
1479
  opencharm dev say <charm> <text> speak through it (testing)
@@ -1796,6 +1818,7 @@ async function serveMcp(input, output, deps) {
1796
1818
  }
1797
1819
  //#endregion
1798
1820
  //#region ../charmd/src/admin/admin-actions.ts
1821
+ const repliesInput = z.object({ speak: z.boolean() }).strict();
1799
1822
  const pairInput = z.object({
1800
1823
  code: z.string().regex(/^\d{6}$/, "the code has 6 digits"),
1801
1824
  pin: z.string().regex(/^\d{4,12}$/, "the PIN is 4 to 12 digits"),
@@ -1829,7 +1852,7 @@ function parseInput(schema, input) {
1829
1852
  if (!result.success) throw new Error(result.error.issues.map((i) => i.message).join("; "));
1830
1853
  return result.data;
1831
1854
  }
1832
- function createAdminActions({ store, pairing, sessions, look, agentCanChangeLook, identity, starter, voice }) {
1855
+ function createAdminActions({ store, pairing, sessions, look, agentCanChangeLook, identity, starter, voice, replies }) {
1833
1856
  function sessionsOf(charmId) {
1834
1857
  return [...sessions()].filter((s) => s.charmId === charmId && s.state !== "closed");
1835
1858
  }
@@ -1934,6 +1957,14 @@ function createAdminActions({ store, pairing, sessions, look, agentCanChangeLook
1934
1957
  };
1935
1958
  });
1936
1959
  },
1960
+ replies(input) {
1961
+ return settled(() => {
1962
+ const { speak } = parseInput(repliesInput, input);
1963
+ if (!replies) throw new Error("This charmd can't change how replies are given.");
1964
+ replies.speak = speak;
1965
+ return { speak };
1966
+ });
1967
+ },
1937
1968
  status() {
1938
1969
  return {
1939
1970
  charmd: identity ?? "unknown",
@@ -1967,7 +1998,8 @@ const COMMANDS = [
1967
1998
  "devFace",
1968
1999
  "devSay",
1969
2000
  "devAsk",
1970
- "look"
2001
+ "look",
2002
+ "replies"
1971
2003
  ];
1972
2004
  function isCommand(value) {
1973
2005
  return typeof value === "string" && COMMANDS.includes(value);
@@ -2248,7 +2280,8 @@ var Session = class {
2248
2280
  sendAudio: (packet) => {
2249
2281
  if (this.#state === "unlocked") this.#deps.outbound.sendAudio?.(packet);
2250
2282
  },
2251
- ask: (text, options) => this.ask(text, options)
2283
+ ask: (text, options) => this.ask(text, options),
2284
+ kind: () => this.#build?.kind
2252
2285
  });
2253
2286
  return this.#turn;
2254
2287
  }
@@ -2327,7 +2360,8 @@ var Session = class {
2327
2360
  type: "hello",
2328
2361
  transport: "websocket",
2329
2362
  session_id: this.sessionId,
2330
- audio_params: AUDIO_DOWN
2363
+ audio_params: AUDIO_DOWN,
2364
+ ...this.#build?.kind === "desktop" ? { features: { text: true } } : {}
2331
2365
  });
2332
2366
  const { token } = this.#deps;
2333
2367
  if (token === void 0) {
@@ -2372,6 +2406,9 @@ var Session = class {
2372
2406
  if (message.state === "start") turn.listenStart();
2373
2407
  else if (message.state === "stop") turn.listenStop();
2374
2408
  } else if (message.type === "abort") turn.abort();
2409
+ else if (message.type === "charm" && message.op === "text") {
2410
+ if (this.#build?.kind === "desktop") turn.typed(message.text);
2411
+ }
2375
2412
  }
2376
2413
  async #onUnlock(pin) {
2377
2414
  if (this.#state === "unlocked") {
@@ -3220,6 +3257,10 @@ var SentenceSplitter = class {
3220
3257
  #buffer = "";
3221
3258
  #pending = "";
3222
3259
  #started = false;
3260
+ #firstClause;
3261
+ constructor(options = {}) {
3262
+ this.#firstClause = options.firstClause ?? true;
3263
+ }
3223
3264
  #take(piece) {
3224
3265
  const text = piece.replace(/\s+/g, " ").trim();
3225
3266
  if (!text) return [];
@@ -3234,7 +3275,7 @@ var SentenceSplitter = class {
3234
3275
  this.#buffer += chunk;
3235
3276
  const out = [];
3236
3277
  let start = 0;
3237
- for (const match of this.#buffer.matchAll(this.#started ? BOUNDARY : FIRST_BOUNDARY)) {
3278
+ for (const match of this.#buffer.matchAll(this.#started || !this.#firstClause ? BOUNDARY : FIRST_BOUNDARY)) {
3238
3279
  const end = match.index + match[0].length;
3239
3280
  if (/^[,;:]/.test(match[0]) && (this.#started || this.#pending.length + end - start < FIRST_CLAUSE_CHARS)) continue;
3240
3281
  const lastWord = this.#buffer.slice(start, end).trim().split(/\s+/).at(-1)?.toLowerCase() ?? "";
@@ -3259,6 +3300,8 @@ function cleanForSpeech(text) {
3259
3300
  //#endregion
3260
3301
  //#region ../charmd/src/turn/turn.ts
3261
3302
  const MAX_FRAMES = 1e3;
3303
+ const READING_WORDS_PER_SECOND = 3;
3304
+ const MIN_READING_MS = 2e3;
3262
3305
  const LOOKAHEAD = 2;
3263
3306
  const AGENT_LABELS = { ...Object.fromEntries(Object.entries(ACP_AGENTS).map(([name, agent]) => [name, agent.label])) };
3264
3307
  var TurnTimeout = class extends Error {};
@@ -3269,6 +3312,10 @@ var StageError = class extends Error {
3269
3312
  this.stage = stage;
3270
3313
  }
3271
3314
  };
3315
+ function readingMs(text) {
3316
+ const words = text.split(/\s+/).filter(Boolean).length;
3317
+ return Math.max(MIN_READING_MS, words / READING_WORDS_PER_SECOND * 1e3);
3318
+ }
3272
3319
  function sleepMs(ms) {
3273
3320
  return new Promise((resolve) => setTimeout(resolve, ms));
3274
3321
  }
@@ -3356,9 +3403,13 @@ var TurnController = class {
3356
3403
  this.#face("idle");
3357
3404
  return Promise.resolve();
3358
3405
  }
3359
- return this.#run(frames);
3406
+ return this.#run({ frames });
3360
3407
  }
3361
- async #run(frames) {
3408
+ typed(text) {
3409
+ if (this.busy) this.#cancel();
3410
+ return this.#run({ text: text.trim() });
3411
+ }
3412
+ async #run(input) {
3362
3413
  const generation = ++this.#generation;
3363
3414
  const live = () => this.#generation === generation;
3364
3415
  const controller = new AbortController();
@@ -3374,6 +3425,7 @@ var TurnController = class {
3374
3425
  let timer = startClock();
3375
3426
  let speakingStarted = false;
3376
3427
  let asking = false;
3428
+ let reading = false;
3377
3429
  const restartClock = () => {
3378
3430
  clearTimeout(timer);
3379
3431
  timer = startClock();
@@ -3384,7 +3436,15 @@ var TurnController = class {
3384
3436
  restartClock();
3385
3437
  },
3386
3438
  touch: () => {
3387
- if (speakingStarted && !asking) restartClock();
3439
+ if (speakingStarted && !asking && !reading) restartClock();
3440
+ },
3441
+ pause: () => {
3442
+ reading = true;
3443
+ clearTimeout(timer);
3444
+ },
3445
+ resume: () => {
3446
+ reading = false;
3447
+ restartClock();
3388
3448
  }
3389
3449
  };
3390
3450
  const timeoutGuard = new Promise((_, reject) => signal.addEventListener("abort", () => {
@@ -3394,14 +3454,17 @@ var TurnController = class {
3394
3454
  this.#state = "thinking";
3395
3455
  this.#face("thinking");
3396
3456
  try {
3397
- const ogg = writeOggOpus(frames, { inputSampleRate: 16e3 });
3398
- heard = await Promise.race([inStage("stt", () => this.#deps.voice.transcribe(ogg, signal)), timeoutGuard]);
3399
- timings.sttMs = now() - started;
3400
- if (!live()) return;
3401
- this.#deps.send({
3402
- type: "stt",
3403
- text: heard
3404
- });
3457
+ if ("text" in input) heard = input.text;
3458
+ else {
3459
+ const ogg = writeOggOpus(input.frames, { inputSampleRate: 16e3 });
3460
+ heard = await Promise.race([inStage("stt", () => this.#deps.voice.transcribe(ogg, signal)), timeoutGuard]);
3461
+ timings.sttMs = now() - started;
3462
+ if (!live()) return;
3463
+ this.#deps.send({
3464
+ type: "stt",
3465
+ text: heard
3466
+ });
3467
+ }
3405
3468
  if (!heard) {
3406
3469
  outcome = "nothing-heard";
3407
3470
  this.#state = "idle";
@@ -3429,7 +3492,7 @@ var TurnController = class {
3429
3492
  signal,
3430
3493
  ...ask ? { ask: (action) => this.ask(`${label} wants to ${action}.`) } : {}
3431
3494
  });
3432
- said = await this.#speak(reply, signal, live, timings, started, timeoutGuard, guessLanguage(heard) ?? this.#deps.voice.language ?? "en", clock);
3495
+ said = await this.#speak(reply, signal, live, timings, started, timeoutGuard, guessLanguage(heard) ?? this.#deps.voice.language ?? "en", clock, !("text" in input) && (this.#deps.speakAloud?.() ?? true));
3433
3496
  if (!live()) return;
3434
3497
  if (this.#speaking) this.#deps.send({
3435
3498
  type: "tts",
@@ -3464,6 +3527,7 @@ var TurnController = class {
3464
3527
  outcome: live() ? outcome : "cancelled",
3465
3528
  ...timings,
3466
3529
  totalMs: now() - started,
3530
+ ..."text" in input ? { typed: true } : {},
3467
3531
  ...this.#deps.logTranscripts ? {
3468
3532
  heard,
3469
3533
  said: said.trim()
@@ -3471,7 +3535,7 @@ var TurnController = class {
3471
3535
  });
3472
3536
  }
3473
3537
  }
3474
- async #speak(source, signal, live, timings, started, timeoutGuard, language, clock) {
3538
+ async #speak(source, signal, live, timings, started, timeoutGuard, language, clock, aloud) {
3475
3539
  const now = this.#deps.now ?? Date.now;
3476
3540
  const going = () => live() && !signal.aborted;
3477
3541
  let speaking = language;
@@ -3497,11 +3561,11 @@ var TurnController = class {
3497
3561
  text,
3498
3562
  language: sentenceLanguage
3499
3563
  });
3500
- startUpTo(playing + LOOKAHEAD);
3564
+ if (aloud) startUpTo(playing + LOOKAHEAD);
3501
3565
  wake();
3502
3566
  };
3503
3567
  const produce = (async () => {
3504
- const splitter = new SentenceSplitter();
3568
+ const splitter = new SentenceSplitter({ firstClause: aloud });
3505
3569
  await inStage("agent", async () => {
3506
3570
  for await (const chunk of source) {
3507
3571
  if (!going()) return;
@@ -3524,6 +3588,11 @@ var TurnController = class {
3524
3588
  await new Promise((resolve) => wake = resolve);
3525
3589
  continue;
3526
3590
  }
3591
+ if (!aloud) {
3592
+ await this.#showForReading(queue[playing].text, signal, going, clock, timings, started);
3593
+ playing += 1;
3594
+ continue;
3595
+ }
3527
3596
  startUpTo(playing + LOOKAHEAD);
3528
3597
  const { text, packets } = queue[playing];
3529
3598
  if (!packets) throw new Error("A sentence wasn't started");
@@ -3563,9 +3632,20 @@ var TurnController = class {
3563
3632
  clearTimeout(timer);
3564
3633
  timer = startClock();
3565
3634
  };
3635
+ let reading = false;
3566
3636
  const clock = {
3567
3637
  started: restartClock,
3568
- touch: restartClock
3638
+ touch: () => {
3639
+ if (!reading) restartClock();
3640
+ },
3641
+ pause: () => {
3642
+ reading = true;
3643
+ clearTimeout(timer);
3644
+ },
3645
+ resume: () => {
3646
+ reading = false;
3647
+ restartClock();
3648
+ }
3569
3649
  };
3570
3650
  const timeoutGuard = new Promise((_, reject) => controller.signal.addEventListener("abort", () => {
3571
3651
  if (controller.signal.reason instanceof TurnTimeout) reject(controller.signal.reason);
@@ -3575,7 +3655,7 @@ var TurnController = class {
3575
3655
  yield await Promise.resolve(text);
3576
3656
  }
3577
3657
  try {
3578
- await this.#speak(once(), controller.signal, live, {}, Date.now(), timeoutGuard, guessLanguage(text) ?? this.#deps.voice.language ?? "en", clock);
3658
+ await this.#speak(once(), controller.signal, live, {}, Date.now(), timeoutGuard, guessLanguage(text) ?? this.#deps.voice.language ?? "en", clock, this.#deps.speakAloud?.() ?? true);
3579
3659
  if (!live()) return;
3580
3660
  if (this.#speaking) this.#deps.send({
3581
3661
  type: "tts",
@@ -3598,6 +3678,28 @@ var TurnController = class {
3598
3678
  clearTimeout(timer);
3599
3679
  }
3600
3680
  }
3681
+ async #showForReading(text, signal, going, clock, timings, started) {
3682
+ if (!going()) return;
3683
+ if (!this.#speaking) {
3684
+ this.#speaking = true;
3685
+ this.#state = "speaking";
3686
+ timings.firstTextMs = (this.#deps.now ?? Date.now)() - started;
3687
+ clock.started();
3688
+ this.#deps.send({
3689
+ type: "tts",
3690
+ state: "start"
3691
+ });
3692
+ }
3693
+ this.#deps.send({
3694
+ type: "tts",
3695
+ state: "sentence_start",
3696
+ text
3697
+ });
3698
+ const sleep = this.#deps.sleep ?? sleepMs;
3699
+ clock.pause();
3700
+ await Promise.race([sleep(readingMs(text)), new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }))]);
3701
+ clock.resume();
3702
+ }
3601
3703
  #failureLine(error) {
3602
3704
  if (error instanceof TurnTimeout) return "That took too long.";
3603
3705
  if (error instanceof StageError && error.cause instanceof VoiceNotReady) return error.cause.message;
@@ -4670,6 +4772,7 @@ async function startDaemon(config, options = {}) {
4670
4772
  });
4671
4773
  const sessions = /* @__PURE__ */ new Map();
4672
4774
  const voice = createVoiceFromConfig(config);
4775
+ const replies = { speak: config.speakReplies };
4673
4776
  voice.warm?.().catch(() => void 0);
4674
4777
  const log = options.log ?? ((entry) => {
4675
4778
  if (!options.quiet) console.log(JSON.stringify({
@@ -4691,6 +4794,7 @@ async function startDaemon(config, options = {}) {
4691
4794
  agentCanChangeLook,
4692
4795
  identity: options.identity,
4693
4796
  starter: options.starter,
4797
+ replies,
4694
4798
  voice: () => {
4695
4799
  const downloading = ["parakeet", "supertonic"].flatMap((id) => {
4696
4800
  const state = modelState(id);
@@ -4749,7 +4853,8 @@ async function startDaemon(config, options = {}) {
4749
4853
  agent,
4750
4854
  timeoutMs: config.turnTimeoutSeconds * 1e3,
4751
4855
  logTranscripts: config.logTranscripts,
4752
- log
4856
+ log,
4857
+ speakAloud: () => io.kind() !== "desktop" || replies.speak
4753
4858
  })
4754
4859
  });
4755
4860
  sessions.set(session.connectionId, session);
@@ -4831,7 +4936,7 @@ function formatIdentity(unit, version, commit) {
4831
4936
  let cached;
4832
4937
  function identity() {
4833
4938
  if (cached) return cached;
4834
- const commit = "42da0c7";
4939
+ const commit = "cb4cbce";
4835
4940
  cached = {
4836
4941
  version: `cli@${version}`,
4837
4942
  commit,
@@ -5311,6 +5416,7 @@ async function main(argv) {
5311
5416
  case "status":
5312
5417
  case "dev":
5313
5418
  case "look":
5419
+ case "replies":
5314
5420
  await runAdminCommand(ctx, command, rest, {
5315
5421
  socket: loadConfig(flagValue(rest, "--config")).adminSocket,
5316
5422
  send: sendAdmin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencharm",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "An open-source body for the AI agent you already run: a face, a voice, one key. Claude Code, Codex, Gemini CLI, Hermes, OpenClaw or any ACP or OpenAI-compatible agent; on a small board, your Mac's notch or the browser.",
5
5
  "type": "module",
6
6
  "bin": {
package/sim/charm_sim.js CHANGED
@@ -1,2 +1,2 @@
1
- async function createCharm(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var programArgs=[];var thisProgram="./this.program";var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["r"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("charm_sim.wasm")}return new URL("charm_sim.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var HEAPU32;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var __Unwind_RaiseException=ex=>{abort()};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;__Unwind_RaiseException(ptr)};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var HEAPU8;var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var _fd_close=fd=>52;var printCharBuffers=[null,[],[]];var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){printChar(fd,HEAPU8[ptr+j])}num+=len}HEAPU32[pnum>>2]=num;return 0};var HEAPF32;var HEAP16;var HEAP32;var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;function js_panel(open){charmSim.panel(!!open)}function js_send_text(json){charmSim.sendText(UTF8ToString(json))}function js_send_binary(data,size){charmSim.sendBinary(HEAPU8.slice(data,data+size))}function js_mic(on){charmSim.mic(!!on)}function js_play_pcm(pcm,samples){charmSim.playPcm(HEAPF32.slice(pcm>>2,(pcm>>2)+samples))}function js_stop_audio(){charmSim.stopAudio()}function js_brightness(percent){charmSim.brightness(percent)}function js_store_get(key){var value=localStorage.getItem("opencharm."+UTF8ToString(key));if(value===null)return 0;var size=lengthBytesUTF8(value)+1;var ptr=_malloc(size);stringToUTF8(value,ptr,size);return ptr}function js_store_set(key,value){localStorage.setItem("opencharm."+UTF8ToString(key),UTF8ToString(value))}function js_store_erase(key){localStorage.removeItem("opencharm."+UTF8ToString(key))}function js_reconnect(){charmSim.reconnect()}var _sim_set_build,_sim_commit,_sim_init,_sim_frame,_sim_pin_key,_sim_frame_box,_sim_frame_dirty,_sim_tick,_sim_pointer,_sim_key,_sim_connected,_sim_disconnected,_sim_text,_sim_audio,_sim_mic_pcm,_sim_token,_sim_forget,_free,_malloc,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_sim_set_build=Module["_sim_set_build"]=wasmExports["s"];_sim_commit=Module["_sim_commit"]=wasmExports["t"];_sim_init=Module["_sim_init"]=wasmExports["u"];_sim_frame=Module["_sim_frame"]=wasmExports["v"];_sim_pin_key=Module["_sim_pin_key"]=wasmExports["w"];_sim_frame_box=Module["_sim_frame_box"]=wasmExports["x"];_sim_frame_dirty=Module["_sim_frame_dirty"]=wasmExports["y"];_sim_tick=Module["_sim_tick"]=wasmExports["z"];_sim_pointer=Module["_sim_pointer"]=wasmExports["A"];_sim_key=Module["_sim_key"]=wasmExports["B"];_sim_connected=Module["_sim_connected"]=wasmExports["C"];_sim_disconnected=Module["_sim_disconnected"]=wasmExports["D"];_sim_text=Module["_sim_text"]=wasmExports["E"];_sim_audio=Module["_sim_audio"]=wasmExports["F"];_sim_mic_pcm=Module["_sim_mic_pcm"]=wasmExports["G"];_sim_token=Module["_sim_token"]=wasmExports["H"];_sim_forget=Module["_sim_forget"]=wasmExports["I"];_free=Module["_free"]=wasmExports["J"];_malloc=Module["_malloc"]=wasmExports["K"];memory=wasmMemory=wasmExports["q"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={o:___cxa_throw,l:__abort_js,j:_emscripten_resize_heap,k:_fd_close,b:_fd_write,e:js_brightness,a:js_mic,p:js_panel,g:js_play_pcm,m:js_reconnect,h:js_send_binary,i:js_send_text,f:js_stop_audio,n:js_store_erase,d:js_store_get,c:js_store_set};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run();
1
+ async function createCharm(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var programArgs=[];var thisProgram="./this.program";var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["r"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("charm_sim.wasm")}return new URL("charm_sim.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var HEAPU32;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var __Unwind_RaiseException=ex=>{abort()};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;__Unwind_RaiseException(ptr)};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var HEAPU8;var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var _fd_close=fd=>52;var printCharBuffers=[null,[],[]];var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){printChar(fd,HEAPU8[ptr+j])}num+=len}HEAPU32[pnum>>2]=num;return 0};var HEAPF32;var HEAP16;var HEAP32;var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;function js_panel(open){charmSim.panel(!!open)}function js_send_text(json){charmSim.sendText(UTF8ToString(json))}function js_send_binary(data,size){charmSim.sendBinary(HEAPU8.slice(data,data+size))}function js_mic(on){charmSim.mic(!!on)}function js_play_pcm(pcm,samples){charmSim.playPcm(HEAPF32.slice(pcm>>2,(pcm>>2)+samples))}function js_stop_audio(){charmSim.stopAudio()}function js_brightness(percent){charmSim.brightness(percent)}function js_store_get(key){var value=localStorage.getItem("opencharm."+UTF8ToString(key));if(value===null)return 0;var size=lengthBytesUTF8(value)+1;var ptr=_malloc(size);stringToUTF8(value,ptr,size);return ptr}function js_store_set(key,value){localStorage.setItem("opencharm."+UTF8ToString(key),UTF8ToString(value))}function js_store_erase(key){localStorage.removeItem("opencharm."+UTF8ToString(key))}function js_reconnect(){charmSim.reconnect()}var _sim_set_build,_sim_commit,_sim_init,_sim_frame,_sim_pin_key,_sim_frame_box,_sim_frame_dirty,_sim_tick,_sim_pointer,_sim_key,_sim_connected,_sim_disconnected,_sim_text,_sim_type,_sim_audio,_sim_mic_pcm,_sim_token,_sim_forget,_free,_malloc,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_sim_set_build=Module["_sim_set_build"]=wasmExports["s"];_sim_commit=Module["_sim_commit"]=wasmExports["t"];_sim_init=Module["_sim_init"]=wasmExports["u"];_sim_frame=Module["_sim_frame"]=wasmExports["v"];_sim_pin_key=Module["_sim_pin_key"]=wasmExports["w"];_sim_frame_box=Module["_sim_frame_box"]=wasmExports["x"];_sim_frame_dirty=Module["_sim_frame_dirty"]=wasmExports["y"];_sim_tick=Module["_sim_tick"]=wasmExports["z"];_sim_pointer=Module["_sim_pointer"]=wasmExports["A"];_sim_key=Module["_sim_key"]=wasmExports["B"];_sim_connected=Module["_sim_connected"]=wasmExports["C"];_sim_disconnected=Module["_sim_disconnected"]=wasmExports["D"];_sim_text=Module["_sim_text"]=wasmExports["E"];_sim_type=Module["_sim_type"]=wasmExports["F"];_sim_audio=Module["_sim_audio"]=wasmExports["G"];_sim_mic_pcm=Module["_sim_mic_pcm"]=wasmExports["H"];_sim_token=Module["_sim_token"]=wasmExports["I"];_sim_forget=Module["_sim_forget"]=wasmExports["J"];_free=Module["_free"]=wasmExports["K"];_malloc=Module["_malloc"]=wasmExports["L"];memory=wasmMemory=wasmExports["q"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={o:___cxa_throw,l:__abort_js,j:_emscripten_resize_heap,k:_fd_close,b:_fd_write,e:js_brightness,a:js_mic,p:js_panel,g:js_play_pcm,m:js_reconnect,h:js_send_binary,i:js_send_text,f:js_stop_audio,n:js_store_erase,d:js_store_get,c:js_store_set};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run();
2
2
  ;return Module}export default createCharm;
Binary file
package/sim/sim.js CHANGED
@@ -109,6 +109,14 @@ window.charmSim = {
109
109
  M._sim_forget();
110
110
  ws?.close();
111
111
  },
112
+ // Typed text from the desktop charm's field (spec 013): "sent", "busy" (a question or a talk), or
113
+ // "unsupported" (an older charmd).
114
+ type: (text) => {
115
+ let result = 0;
116
+ withString(text, (ptr) => (result = M._sim_type(ptr)));
117
+ wake();
118
+ return ["busy", "sent", "unsupported"][result] ?? "busy";
119
+ },
112
120
  // A PIN key from a keyboard or the desktop app ("0"…"9", "<", "OK").
113
121
  pinKey: (key) => {
114
122
  withString(key, (ptr) => M._sim_pin_key(ptr));
@@ -291,15 +299,22 @@ function key(down) {
291
299
  wake();
292
300
  }
293
301
 
302
+ // Keys typed into a text field (the desktop charm's typing field) are text, not the charm's key or PIN.
303
+ const typingInto = (e) =>
304
+ e.target instanceof HTMLElement &&
305
+ (e.target.isContentEditable ||
306
+ e.target.tagName === "INPUT" ||
307
+ e.target.tagName === "TEXTAREA");
308
+
294
309
  let spaceDown = false;
295
310
  addEventListener("keydown", (e) => {
296
- if (e.code !== "Space" || e.repeat || spaceDown) return;
311
+ if (e.code !== "Space" || e.repeat || spaceDown || typingInto(e)) return;
297
312
  e.preventDefault();
298
313
  spaceDown = true;
299
314
  key(true);
300
315
  });
301
316
  addEventListener("keyup", (e) => {
302
- if (e.code !== "Space") return;
317
+ if (e.code !== "Space" || !spaceDown) return;
303
318
  e.preventDefault();
304
319
  spaceDown = false;
305
320
  key(false);
@@ -349,6 +364,7 @@ document.getElementById("shape").addEventListener("click", () => {
349
364
 
350
365
  // The PIN can be typed on the keyboard too (the notch has no room for a pad).
351
366
  addEventListener("keydown", (e) => {
367
+ if (typingInto(e)) return;
352
368
  const key = /^[0-9]$/.test(e.key)
353
369
  ? e.key
354
370
  : e.key === "Backspace"