@fraczak/k 6.7.0 → 6.9.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.
@@ -3,30 +3,36 @@ import {
3
3
  evaluateInput,
4
4
  createCompleter,
5
5
  promptForState,
6
- helpText,
7
6
  analyzeRawSnippet,
8
7
  lineTerminatesSnippet,
9
8
  lineHasExplicitContinuation,
10
9
  savedLibrary,
11
- listCodecs,
12
- loadCodecModule,
13
10
  registerCodec,
14
11
  unregisterCodec,
15
12
  resolveCodec,
16
13
  codeHashToPattern,
17
- BUILTIN_CODECS,
18
- formatDuration
14
+ formatDuration,
15
+ resolveInputTypeHash,
16
+ resolveInputPattern,
17
+ valueForPattern,
18
+ codecNames,
19
+ ensureEnveloped,
20
+ patternFromFilter
19
21
  } from "../repl.mjs";
20
- import { Value, isProduct, isVariant } from "../Value.mjs";
22
+ import { Value, isProduct, isVariant, withPattern } from "../Value.mjs";
21
23
  import { encodeLibrary } from "../object.mjs";
22
24
  import { setVfsFile, getVfsFile, getAllVfsFiles } from "./shims/fs.mjs";
23
25
  import { createOutputEntryElement } from "./tree-view.mjs";
24
26
 
25
- // Make Value and helpers globally available for custom codecs
27
+ // Make Value and helpers globally available in browser console
26
28
  if (typeof window !== "undefined") {
27
- window.Value = Value;
28
- window.isProduct = isProduct;
29
- window.isVariant = isVariant;
29
+ Object.assign(window, {
30
+ Value,
31
+ isProduct,
32
+ isVariant,
33
+ withPattern,
34
+ patternFromFilter
35
+ });
30
36
  }
31
37
 
32
38
  // Global REPL state
@@ -61,17 +67,8 @@ let helpModal;
61
67
  let dropOverlay;
62
68
  let codecsModal;
63
69
  let codecsBadgeEl;
64
- let activeCodecsTbody;
65
- let codecTypeSelect;
66
-
67
- // Input Popup Modal Elements
68
- let inputPopupModal;
69
- let inputPopupType;
70
- let inputPopupCodec;
71
- let inputPopupText;
72
- let inputPopupStatus;
73
- let inputPopupHint;
74
- let inputSamplesList;
70
+ let enabledCodecsGrid;
71
+
75
72
 
76
73
  function escapeHtml(str) {
77
74
  return String(str)
@@ -115,17 +112,7 @@ function ansiToHtml(str) {
115
112
 
116
113
  function countActiveCodecs() {
117
114
  const store = state.codecs || {};
118
- let count = 0;
119
- const seen = new Set();
120
- for (const list of Object.values(store)) {
121
- for (const c of list) {
122
- if (!seen.has(c.name)) {
123
- seen.add(c.name);
124
- count++;
125
- }
126
- }
127
- }
128
- return count;
115
+ return Object.keys(store).length;
129
116
  }
130
117
 
131
118
  function updateCodecsBadge() {
@@ -264,12 +251,6 @@ export async function executeCommand(input) {
264
251
  const trimmed = input.trim();
265
252
  if (!trimmed && !state.pendingInput) return;
266
253
 
267
- // Intercept :input without args -> open input popup
268
- if (trimmed === ":input") {
269
- openInputPopup();
270
- return;
271
- }
272
-
273
254
  // Add to history
274
255
  if (history.length === 0 || history[history.length - 1] !== input) {
275
256
  history.push(input);
@@ -324,12 +305,6 @@ export async function executeCommand(input) {
324
305
  if (codecsModal && codecsModal.classList.contains("open")) {
325
306
  renderCodecsModal();
326
307
  }
327
-
328
- // If :input <type> [codec] was executed, state.pendingInput is now set!
329
- // Open the Input Popup immediately to prompt the user for the input string!
330
- if (errors.length === 0 && state.pendingInput && trimmed.startsWith(":input ")) {
331
- openInputPopup();
332
- }
333
308
  }
334
309
 
335
310
  function adjustInputHeight() {
@@ -353,14 +328,7 @@ function handleAutocomplete() {
353
328
  }
354
329
 
355
330
  if (matches.length === 1) {
356
- hideAutocomplete();
357
- const tokenStart = lineBeforeCursor.lastIndexOf(" ") + 1;
358
- const match = matches[0];
359
- const beforeToken = text.slice(0, cursorPos - (lineBeforeCursor.length - tokenStart));
360
- const afterCursor = text.slice(cursorPos);
361
- inputEl.value = beforeToken + match + (afterCursor.startsWith(" ") ? "" : " ") + afterCursor;
362
- inputEl.selectionStart = inputEl.selectionEnd = beforeToken.length + match.length + 1;
363
- adjustInputHeight();
331
+ applyAutocomplete(matches[0]);
364
332
  return;
365
333
  }
366
334
 
@@ -402,11 +370,12 @@ function applyAutocomplete(match) {
402
370
  const text = inputEl.value;
403
371
  const cursorPos = inputEl.selectionStart;
404
372
  const lineBeforeCursor = text.slice(0, cursorPos).split("\n").pop();
405
- const tokenStart = Math.max(0, lineBeforeCursor.lastIndexOf(" ") + 1);
406
- const beforeToken = text.slice(0, cursorPos - (lineBeforeCursor.length - tokenStart));
373
+ const lineStart = cursorPos - lineBeforeCursor.length;
374
+ const beforeLine = text.slice(0, lineStart);
407
375
  const afterCursor = text.slice(cursorPos);
408
- inputEl.value = beforeToken + match + (afterCursor.startsWith(" ") ? "" : " ") + afterCursor;
409
- inputEl.selectionStart = inputEl.selectionEnd = beforeToken.length + match.length + 1;
376
+ const insertSpace = (match.endsWith("/") || afterCursor.startsWith(" ")) ? "" : " ";
377
+ inputEl.value = beforeLine + match + insertSpace + afterCursor;
378
+ inputEl.selectionStart = inputEl.selectionEnd = beforeLine.length + match.length + insertSpace.length;
410
379
  hideAutocomplete();
411
380
  adjustInputHeight();
412
381
  inputEl.focus();
@@ -566,97 +535,6 @@ function exportKlib() {
566
535
  // CODECS SUBSYSTEM UI & CONTROLS
567
536
  // ==========================================
568
537
 
569
- const CODEC_PRESETS = {
570
- yn: {
571
- name: "yn",
572
- type: "bool",
573
- universal: false,
574
- parse: `// Parse 'yes', 'no', 'true', 'false' into $bool
575
- const tag = text.trim().toLowerCase();
576
- if (tag === "yes" || tag === "true" || tag === "1") {
577
- return Value.variant("true", Value.product({}));
578
- }
579
- if (tag === "no" || tag === "false" || tag === "0") {
580
- return Value.variant("false", Value.product({}));
581
- }
582
- throw new Error("Expected yes/no or true/false");`,
583
- print: `// Serialize boolean value to YES / NO
584
- return value.tag === "true" ? "YES" : "NO";`
585
- },
586
- hex: {
587
- name: "hex",
588
- type: "int",
589
- universal: false,
590
- parse: `// Parse decimal or hex (e.g. 0x2a, 42, -0x10) into $int
591
- let str = text.trim();
592
- let sign = "+";
593
- if (str.startsWith("-")) { sign = "-"; str = str.slice(1).trim(); }
594
- else if (str.startsWith("+")) { str = str.slice(1).trim(); }
595
-
596
- const num = str.startsWith("0x") || str.startsWith("0X") ? BigInt(str) : BigInt(str);
597
- if (num === 0n) sign = "+";
598
-
599
- const bitChars = num === 0n ? ["0"] : num.toString(2).split("");
600
- let v = Value.variant("_", Value.product({}));
601
- for (let i = bitChars.length - 1; i >= 0; i--) {
602
- v = Value.variant(bitChars[i], v);
603
- }
604
- return Value.variant(sign, v);`,
605
- print: `// Serialize $int to hexadecimal string (0x...)
606
- if (!isVariant(value) || (value.tag !== "+" && value.tag !== "-")) {
607
- throw new Error("Not a valid int value");
608
- }
609
- let bits = "";
610
- let node = value.value;
611
- while (isVariant(node) && node.tag !== "_") {
612
- bits += node.tag;
613
- node = node.value;
614
- }
615
- const n = bits === "" ? 0n : BigInt("0b" + bits);
616
- const hex = "0x" + n.toString(16).toUpperCase();
617
- return (value.tag === "-" && n !== 0n ? "-" : "") + hex;`
618
- },
619
- currency: {
620
- name: "currency",
621
- type: "int",
622
- universal: false,
623
- parse: `// Parse currency e.g. $42.50 or $100 into cents int
624
- const clean = text.trim().replace(/^\\$/, "").trim();
625
- const parts = clean.split(".");
626
- const dollars = BigInt(parts[0] || "0");
627
- const cents = BigInt((parts[1] || "0").padEnd(2, "0").slice(0, 2));
628
- const total = dollars * 100n + cents;
629
-
630
- const bitChars = total === 0n ? ["0"] : total.toString(2).split("");
631
- let v = Value.variant("_", Value.product({}));
632
- for (let i = bitChars.length - 1; i >= 0; i--) {
633
- v = Value.variant(bitChars[i], v);
634
- }
635
- return Value.variant("+", v);`,
636
- print: `// Format cents int as currency string ($X.XX)
637
- let bits = "";
638
- let node = value.value;
639
- while (isVariant(node) && node.tag !== "_") {
640
- bits += node.tag;
641
- node = node.value;
642
- }
643
- const n = bits === "" ? 0n : BigInt("0b" + bits);
644
- const dollars = n / 100n;
645
- const cents = (n % 100n).toString().padStart(2, "0");
646
- return "$" + dollars.toString() + "." + cents;`
647
- },
648
- blank: {
649
- name: "custom",
650
- type: "",
651
- universal: false,
652
- parse: `// Parse text string and return a K Value
653
- // Available: Value.product({ field: val }), Value.variant("tag", val)
654
- return Value.product({});`,
655
- print: `// Serialize K Value into string or Buffer
656
- return JSON.stringify(value);`
657
- }
658
- };
659
-
660
538
  function openCodecsModal() {
661
539
  if (!codecsModal) return;
662
540
  renderCodecsModal();
@@ -669,501 +547,105 @@ function closeCodecsModal() {
669
547
 
670
548
  function renderCodecsModal() {
671
549
  updateCodecsBadge();
550
+ const grid = enabledCodecsGrid || document.getElementById("enabled-codecs-grid");
551
+ if (!grid) return;
552
+ grid.innerHTML = "";
672
553
 
673
- // 1. Built-in codec buttons state
674
- const standardCodecs = ["int", "utf8", "json", "ieee"];
675
- for (const name of standardCodecs) {
676
- const btn = document.getElementById(`btn-toggle-codec-${name}`);
677
- const badge = document.getElementById(`codec-badge-${name}`);
678
- const isLoaded = isCodecRegistered(name);
679
- if (btn) {
680
- btn.textContent = isLoaded ? "Unload" : "Load";
681
- btn.className = isLoaded ? "btn btn-sm btn-secondary" : "btn btn-sm btn-primary";
682
- }
683
- if (badge) {
684
- badge.textContent = isLoaded ? "Active" : "Inactive";
685
- badge.className = isLoaded ? "codec-status-badge active" : "codec-status-badge";
686
- }
687
- }
688
-
689
- // 2. Populate target type dropdown
690
- if (codecTypeSelect) {
691
- const prev = codecTypeSelect.value;
692
- codecTypeSelect.innerHTML = `<option value="">-- Choose Type Alias or Code Hash --</option>`;
693
- const typeGroup = document.createElement("optgroup");
694
- typeGroup.label = "Available Type Aliases in State";
695
- const aliases = Object.entries(state.typeAliases || {}).sort(([a], [b]) => a.localeCompare(b));
696
- for (const [alias, hash] of aliases) {
697
- const opt = document.createElement("option");
698
- opt.value = alias;
699
- opt.textContent = `$${alias} (${hash.slice(0, 10)}...)`;
700
- typeGroup.appendChild(opt);
701
- }
702
- codecTypeSelect.appendChild(typeGroup);
703
-
704
- const universalOpt = document.createElement("option");
705
- universalOpt.value = "*";
706
- universalOpt.textContent = "* (Universal - all types)";
707
- codecTypeSelect.appendChild(universalOpt);
708
-
709
- if (prev) codecTypeSelect.value = prev;
710
- }
711
-
712
- // 3. Render active codecs table
713
- if (activeCodecsTbody) {
714
- activeCodecsTbody.innerHTML = "";
715
- const store = state.codecs || {};
716
- const entries = [];
717
- for (const [codeHash, list] of Object.entries(store)) {
718
- for (const c of list) {
719
- entries.push({ codeHash, ...c });
720
- }
721
- }
722
-
723
- if (entries.length === 0) {
724
- const tr = document.createElement("tr");
725
- tr.innerHTML = `<td colspan="5" style="text-align:center;color:var(--text-muted);padding:16px;">No codecs currently loaded. Load a built-in codec or register a custom serializer/deserializer below.</td>`;
726
- activeCodecsTbody.appendChild(tr);
727
- return;
728
- }
729
-
730
- // Invert type aliases for display
731
- const hashToAlias = {};
732
- for (const [name, hash] of Object.entries(state.typeAliases || {})) {
733
- hashToAlias[hash] = name;
734
- }
735
-
736
- for (const entry of entries) {
737
- const tr = document.createElement("tr");
738
-
739
- const nameTd = document.createElement("td");
740
- nameTd.className = "codec-cell-name";
741
- nameTd.textContent = entry.name;
742
- tr.appendChild(nameTd);
743
-
744
- const typeTd = document.createElement("td");
745
- typeTd.className = "codec-cell-type";
746
- if (entry.codeHash === "*") {
747
- typeTd.innerHTML = `<span class="badge-type universal">Universal (*)</span>`;
748
- } else {
749
- const alias = hashToAlias[entry.codeHash];
750
- typeTd.innerHTML = `<span class="badge-type">${alias ? `$${alias} ` : ""}<code title="${entry.codeHash}">${entry.codeHash.slice(0, 10)}...</code></span>`;
751
- }
752
- tr.appendChild(typeTd);
753
-
754
- const capTd = document.createElement("td");
755
- const caps = [];
756
- if (typeof entry.print === "function") caps.push(`<span class="cap-badge print">Serializer</span>`);
757
- if (typeof entry.parse === "function") caps.push(`<span class="cap-badge parse">Deserializer</span>`);
758
- capTd.innerHTML = caps.join(" ");
759
- tr.appendChild(capTd);
760
-
761
- const srcTd = document.createElement("td");
762
- srcTd.className = "codec-cell-src";
763
- srcTd.textContent = entry.source ? (entry.source === "built-in" ? "Built-in" : entry.source.split("/").pop()) : "Custom";
764
- tr.appendChild(srcTd);
765
-
766
- const actTd = document.createElement("td");
767
- const unloadBtn = document.createElement("button");
768
- unloadBtn.className = "btn btn-sm btn-danger";
769
- unloadBtn.textContent = "Unload";
770
- unloadBtn.onclick = () => {
771
- executeCommand(`:codec unload ${entry.name}`);
772
- renderCodecsModal();
773
- };
774
- actTd.appendChild(unloadBtn);
775
- tr.appendChild(actTd);
776
-
777
- activeCodecsTbody.appendChild(tr);
778
- }
779
- }
780
- }
781
-
782
- function isCodecRegistered(name) {
783
554
  const store = state.codecs || {};
784
- for (const list of Object.values(store)) {
785
- if (list.some(c => c.name === name)) return true;
786
- }
787
- return false;
788
- }
789
-
790
- function applyCodecPreset(presetKey) {
791
- const preset = CODEC_PRESETS[presetKey];
792
- if (!preset) return;
793
- const nameInput = document.getElementById("codec-custom-name");
794
- const parseArea = document.getElementById("codec-custom-parse");
795
- const printArea = document.getElementById("codec-custom-print");
796
-
797
- if (nameInput) nameInput.value = preset.name;
798
- if (parseArea) parseArea.value = preset.parse;
799
- if (printArea) printArea.value = preset.print;
800
- if (codecTypeSelect) {
801
- if (preset.type) {
802
- codecTypeSelect.value = preset.type;
803
- }
804
- }
805
- }
806
-
807
- function registerCustomCodecFromForm() {
808
- const nameInput = document.getElementById("codec-custom-name");
809
- const parseArea = document.getElementById("codec-custom-parse");
810
- const printArea = document.getElementById("codec-custom-print");
811
- const statusBox = document.getElementById("codec-studio-status");
812
-
813
- const name = nameInput?.value.trim();
814
- const rawType = codecTypeSelect?.value.trim() || "*";
815
- const parseCode = parseArea?.value.trim();
816
- const printCode = printArea?.value.trim();
817
-
818
- if (!name) {
819
- if (statusBox) statusBox.innerHTML = `<span class="line-error">Codec name is required.</span>`;
555
+ const entries = Object.values(store).sort((a, b) => a.name.localeCompare(b.name));
556
+
557
+ if (entries.length === 0) {
558
+ const empty = document.createElement("div");
559
+ empty.className = "codecs-empty-state";
560
+ empty.style.gridColumn = "1 / -1";
561
+ empty.style.padding = "32px 16px";
562
+ empty.style.textAlign = "center";
563
+ empty.style.color = "var(--text-muted)";
564
+ empty.style.background = "var(--bg-base)";
565
+ empty.style.border = "1px dashed var(--border-color)";
566
+ empty.style.borderRadius = "6px";
567
+ empty.innerHTML = `
568
+ <div style="font-size: 24px; margin-bottom: 8px;">📦</div>
569
+ <div style="font-size: 13px; font-weight: 600; color: var(--text-main); margin-bottom: 6px;">No codecs currently loaded</div>
570
+ <div style="font-size: 12px; line-height: 1.5;">Codecs are ordinary <code>.mjs</code> files.<br>Load a codec using <code>:codec load &lt;file.mjs&gt;</code> in the terminal, or select a codec file in <b>Files</b> and click <b>Load into REPL</b>.</div>
571
+ `;
572
+ grid.appendChild(empty);
820
573
  return;
821
574
  }
822
575
 
823
- try {
824
- let parseFn = null;
825
- let printFn = null;
826
-
827
- if (parseCode) {
828
- parseFn = new Function(
829
- "text", "context",
830
- `const { Value, isProduct, isVariant } = globalThis;\n${parseCode}`
831
- );
832
- }
833
-
834
- if (printCode) {
835
- printFn = new Function(
836
- "value", "context",
837
- `const { Value, isProduct, isVariant } = globalThis;\n${printCode}`
838
- );
839
- }
840
-
841
- let codeHash = rawType === "*" ? "*" : state.typeAliases[rawType] || rawType;
842
- if (!codeHash.startsWith("@") && codeHash !== "*") {
843
- throw new Error(`Cannot resolve type '${rawType}' to a canonical code hash. Define the type first (e.g. :type ${rawType} = ...).`);
844
- }
845
-
846
- registerCodec(state, {
847
- name,
848
- codes: [codeHash],
849
- universal: codeHash === "*",
850
- parse: parseFn,
851
- print: printFn
852
- }, "<custom-studio>");
853
-
854
- if (statusBox) {
855
- statusBox.innerHTML = `<span class="line-output">✓ Successfully registered codec <b>${name}</b> for ${codeHash === "*" ? "all types" : codeHash}!</span>`;
856
- }
857
-
858
- appendSystemMessage(`⚙ Registered custom codec <b>${name}</b> (serializer/deserializer) for ${codeHash === "*" ? "all types" : codeHash}.`);
859
- renderCodecsModal();
860
- } catch (err) {
861
- if (statusBox) {
862
- statusBox.innerHTML = `<span class="line-error">Error: ${escapeHtml(err.message)}</span>`;
863
- }
864
- }
865
- }
866
-
867
- function testCustomParse() {
868
- const parseArea = document.getElementById("codec-custom-parse");
869
- const testInput = document.getElementById("codec-test-input");
870
- const statusBox = document.getElementById("codec-studio-status");
871
-
872
- try {
873
- const parseFn = new Function(
874
- "text", "context",
875
- `const { Value, isProduct, isVariant } = globalThis;\n${parseArea.value}`
876
- );
877
- const result = parseFn(testInput.value, { Value, isProduct, isVariant, state });
878
- if (statusBox) {
879
- statusBox.innerHTML = `<span class="line-output"><b>Parse Result:</b> ${escapeHtml(JSON.stringify(result, null, 2))}</span>`;
880
- }
881
- } catch (err) {
882
- if (statusBox) {
883
- statusBox.innerHTML = `<span class="line-error">Parse Error: ${escapeHtml(err.message)}</span>`;
884
- }
885
- }
886
- }
887
-
888
- function testCustomPrint() {
889
- const printArea = document.getElementById("codec-custom-print");
890
- const statusBox = document.getElementById("codec-studio-status");
891
-
892
- try {
893
- const printFn = new Function(
894
- "value", "context",
895
- `const { Value, isProduct, isVariant } = globalThis;\n${printArea.value}`
896
- );
897
- const result = printFn(state.value, { Value, isProduct, isVariant, state });
898
- if (statusBox) {
899
- statusBox.innerHTML = `<span class="line-output"><b>Print Output:</b> ${escapeHtml(String(result))}</span>`;
900
- }
901
- } catch (err) {
902
- if (statusBox) {
903
- statusBox.innerHTML = `<span class="line-error">Print Error: ${escapeHtml(err.message)}</span>`;
904
- }
905
- }
906
- }
907
-
908
- function saveCustomCodecToVfs() {
909
- const nameInput = document.getElementById("codec-custom-name");
910
- const parseArea = document.getElementById("codec-custom-parse");
911
- const printArea = document.getElementById("codec-custom-print");
912
- const statusBox = document.getElementById("codec-studio-status");
913
-
914
- const name = nameInput?.value.trim() || "custom";
915
- const rawType = codecTypeSelect?.value.trim() || "*";
916
- const codeHash = rawType === "*" ? "*" : state.typeAliases[rawType] || rawType;
917
- const fileName = `codecs/${name}-codec.mjs`;
918
-
919
- const fileContent = `// Custom K Codec: ${name}
920
- import { Value, isProduct, isVariant } from "../Value.mjs";
921
-
922
- export const name = ${JSON.stringify(name)};
923
- export const codes = [${JSON.stringify(codeHash)}];
924
- export const universal = ${codeHash === "*"};
925
-
926
- export function parse(text, context) {
927
- ${parseArea?.value || ""}
928
- }
929
-
930
- export function print(value, context) {
931
- ${printArea?.value || ""}
932
- }
933
- `;
934
-
935
- setVfsFile(fileName, fileContent);
936
- if (statusBox) {
937
- statusBox.innerHTML = `<span class="line-output">💾 Saved codec file to VFS as <b>${fileName}</b></span>`;
938
- }
939
- appendSystemMessage(`💾 Saved codec file to VFS: <b>${fileName}</b>. You can reload it anytime via <code>:codec load ${fileName}</code>.`);
940
- }
941
-
942
- // ==========================================
943
- // CODEC INPUT POPUP SUBSYSTEM (:input)
944
- // ==========================================
945
-
946
- const CODEC_SAMPLES = {
947
- int: ["0", "10", "42", "-15", "1000000"],
948
- utf8: ["hello", "hello world", "k repl", "λ"],
949
- json: ['{"name":"Alice"}', '{"x":10,"y":20}', 'true', '[1, 2, 3]'],
950
- ieee: ["0.0", "3.14159", "-2.718", "1e6"],
951
- yn: ["yes", "no", "true", "false"],
952
- hex: ["0x2A", "0xFF", "0x1000", "-0x10"],
953
- currency: ["$10.00", "$42.50", "$99.99"]
954
- };
955
-
956
- export function openInputPopup(preselectedType = null, preselectedCodec = null) {
957
- if (!inputPopupModal) return;
958
-
959
- populateInputPopupDropdowns(preselectedType, preselectedCodec);
960
-
961
- if (inputPopupText) {
962
- inputPopupText.value = "";
963
- }
964
- updateLiveValidation();
965
-
966
- inputPopupModal.classList.add("open");
967
- setTimeout(() => {
968
- if (inputPopupText) inputPopupText.focus();
969
- }, 50);
970
- }
971
-
972
- export function closeInputPopup() {
973
- if (inputPopupModal) {
974
- inputPopupModal.classList.remove("open");
975
- }
976
- if (inputEl) inputEl.focus();
977
- }
978
-
979
- export function cancelInputPopup() {
980
- if (state.pendingInput) {
981
- state.pendingInput = null;
982
- updatePrompt();
983
- appendSystemMessage("Input cancelled.");
984
- }
985
- closeInputPopup();
986
- }
987
-
988
- function renderInputSamples(codecName) {
989
- if (!inputSamplesList) return;
990
- inputSamplesList.innerHTML = "";
991
- const samples = CODEC_SAMPLES[codecName] || ["()"];
992
- for (const s of samples) {
993
- const btn = document.createElement("span");
994
- btn.className = "sample-pill";
995
- btn.textContent = s;
996
- btn.title = `Click to insert sample "${s}"`;
997
- btn.onclick = () => {
998
- if (inputPopupText) {
999
- inputPopupText.value = s;
1000
- inputPopupText.focus();
1001
- updateLiveValidation();
1002
- }
576
+ for (const entry of entries) {
577
+ const card = document.createElement("div");
578
+ card.className = "codec-card active";
579
+ card.id = `card-codec-${entry.name.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
580
+
581
+ const titleEl = document.createElement("div");
582
+ titleEl.className = "codec-card-title";
583
+ titleEl.innerHTML = `
584
+ <span class="codec-icon">📦</span>
585
+ <span class="codec-name">${escapeHtml(entry.name)}</span>
586
+ <span class="codec-status-badge active">Active</span>
587
+ `;
588
+ card.appendChild(titleEl);
589
+
590
+ let targetDesc = "Self-describing";
591
+ if (entry.name === "int.mjs") {
592
+ targetDesc = "Pattern Family: $ int & lists";
593
+ } else if (entry.name === "utf8.mjs") {
594
+ targetDesc = "$ string";
595
+ } else if (entry.name === "ieee.mjs") {
596
+ targetDesc = "$ float64";
597
+ } else if (entry.name === "unit.mjs") {
598
+ targetDesc = "{}";
599
+ } else if (entry.name === "json.mjs") {
600
+ targetDesc = "Self-describing JSON";
601
+ } else if (Array.isArray(entry.patterns) && entry.patterns.length > 0) {
602
+ targetDesc = `Pattern Family (${entry.patterns.length})`;
603
+ } else if (entry.pattern) {
604
+ targetDesc = "Pattern Recipe";
605
+ }
606
+
607
+ const typeDiv = document.createElement("div");
608
+ typeDiv.className = "codec-types";
609
+ typeDiv.innerHTML = `Target: <span class="badge-type">${escapeHtml(targetDesc)}</span>`;
610
+ card.appendChild(typeDiv);
611
+
612
+ const descDiv = document.createElement("div");
613
+ descDiv.className = "codec-desc";
614
+ const src = entry.source || entry.name;
615
+ descDiv.innerHTML = `Source: <code>${escapeHtml(src)}</code>`;
616
+ card.appendChild(descDiv);
617
+
618
+ const footer = document.createElement("div");
619
+ footer.className = "codec-card-footer";
620
+
621
+ const capsDiv = document.createElement("div");
622
+ capsDiv.className = "codec-caps";
623
+ const caps = [];
624
+ if (typeof entry.print === "function") caps.push('<span class="cap-badge print">Serializer</span>');
625
+ if (typeof entry.parse === "function") caps.push('<span class="cap-badge parse">Deserializer</span>');
626
+ capsDiv.innerHTML = caps.join(" ");
627
+ footer.appendChild(capsDiv);
628
+
629
+ const unloadBtn = document.createElement("button");
630
+ unloadBtn.className = "btn btn-sm btn-danger";
631
+ unloadBtn.textContent = "Unload";
632
+ unloadBtn.onclick = async () => {
633
+ await executeCommand(`:codec unload ${entry.name}`);
634
+ renderCodecsModal();
1003
635
  };
1004
- inputSamplesList.appendChild(btn);
1005
- }
1006
- }
636
+ footer.appendChild(unloadBtn);
1007
637
 
1008
- function updateInputPopupHint() {
1009
- if (!inputPopupHint) return;
1010
- if (state.pendingInput) {
1011
- const rawType = inputPopupType?.value || state.pendingInput.codeHash;
1012
- const codecName = inputPopupCodec?.value || state.pendingInput.codecName;
1013
- inputPopupHint.innerHTML = `Enter input value for <b>$${escapeHtml(rawType)}</b> using codec <b>${escapeHtml(codecName || "default")}</b>:`;
1014
- } else {
1015
- inputPopupHint.innerHTML = `Choose target type &amp; deserializer, then enter input string:`;
638
+ card.appendChild(footer);
639
+ grid.appendChild(card);
1016
640
  }
1017
641
  }
1018
642
 
1019
- function updateInputPopupCodecs() {
1020
- if (!inputPopupCodec) return;
1021
- const rawType = inputPopupType?.value || "*";
1022
- const codeHash = rawType === "*" ? "*" : state.typeAliases?.[rawType] || rawType;
1023
- const prev = inputPopupCodec.value;
1024
-
1025
- inputPopupCodec.innerHTML = "";
1026
- const matchingCodecs = [];
643
+ function isCodecRegistered(name) {
1027
644
  const store = state.codecs || {};
1028
-
1029
- const candidates = [
1030
- ...(store[codeHash] || []),
1031
- ...(codeHash !== "*" ? (store["*"] || []) : [])
1032
- ];
1033
-
1034
- const seen = new Set();
1035
- for (const c of candidates) {
1036
- if (typeof c.parse === "function" && !seen.has(c.name)) {
1037
- seen.add(c.name);
1038
- matchingCodecs.push(c.name);
1039
- }
1040
- }
1041
-
1042
- if (matchingCodecs.length === 0) {
1043
- if (rawType === "int") matchingCodecs.push("int");
1044
- if (rawType === "bool") matchingCodecs.push("yn");
1045
- if (rawType === "string") matchingCodecs.push("utf8");
1046
- matchingCodecs.push("json");
1047
- }
1048
-
1049
- for (const name of matchingCodecs) {
1050
- const opt = document.createElement("option");
1051
- opt.value = name;
1052
- opt.textContent = name;
1053
- inputPopupCodec.appendChild(opt);
1054
- }
1055
-
1056
- if (prev && matchingCodecs.includes(prev)) {
1057
- inputPopupCodec.value = prev;
1058
- } else if (matchingCodecs.length > 0) {
1059
- inputPopupCodec.value = matchingCodecs[0];
1060
- }
1061
-
1062
- renderInputSamples(inputPopupCodec.value);
1063
- updateInputPopupHint();
1064
- updateLiveValidation();
645
+ return Boolean(store[name]);
1065
646
  }
1066
647
 
1067
- function populateInputPopupDropdowns(preselectedType = null, preselectedCodec = null) {
1068
- if (!inputPopupType) return;
1069
-
1070
- let prevType = preselectedType;
1071
- if (!prevType && state.pendingInput) {
1072
- const foundAlias = Object.entries(state.typeAliases || {}).find(([, h]) => h === state.pendingInput.codeHash);
1073
- prevType = foundAlias ? foundAlias[0] : state.pendingInput.codeHash;
1074
- }
1075
- if (!prevType) {
1076
- prevType = inputPopupType.value;
1077
- }
1078
-
1079
- inputPopupType.innerHTML = "";
1080
-
1081
- const aliases = Object.entries(state.typeAliases || {}).sort(([a], [b]) => a.localeCompare(b));
1082
- for (const [alias, hash] of aliases) {
1083
- const opt = document.createElement("option");
1084
- opt.value = alias;
1085
- opt.textContent = `$${alias} (${hash.slice(0, 10)}...)`;
1086
- inputPopupType.appendChild(opt);
1087
- }
1088
-
1089
- const universalOpt = document.createElement("option");
1090
- universalOpt.value = "*";
1091
- universalOpt.textContent = "* (Universal / Any)";
1092
- inputPopupType.appendChild(universalOpt);
1093
-
1094
- if (prevType && (aliases.some(([a]) => a === prevType) || prevType === "*")) {
1095
- inputPopupType.value = prevType;
1096
- } else if (aliases.length > 0) {
1097
- inputPopupType.value = aliases[0][0];
1098
- }
1099
-
1100
- updateInputPopupCodecs();
1101
648
 
1102
- const targetCodec = preselectedCodec || state.pendingInput?.codecName;
1103
- if (targetCodec && inputPopupCodec) {
1104
- inputPopupCodec.value = targetCodec;
1105
- renderInputSamples(targetCodec);
1106
- }
1107
- }
1108
-
1109
- function updateLiveValidation() {
1110
- if (!inputPopupStatus) return;
1111
- const text = inputPopupText ? inputPopupText.value : "";
1112
- if (!text) {
1113
- inputPopupStatus.innerHTML = `<span style="color:var(--text-muted);font-size:11px;">Ready for input.</span>`;
1114
- return;
1115
- }
1116
-
1117
- const rawType = inputPopupType?.value;
1118
- const codecName = inputPopupCodec?.value;
1119
- if (!rawType) {
1120
- inputPopupStatus.innerHTML = `<span class="line-warning" style="margin:0;padding:2px 6px;">Select a target type first.</span>`;
1121
- return;
1122
- }
1123
-
1124
- const codeHash = rawType === "*" ? "*" : state.typeAliases?.[rawType] || rawType;
1125
- try {
1126
- const codec = resolveCodec(state, codeHash, codecName || null, "parse");
1127
- const pattern = codeHashToPattern(codeHash, (h) => state.codes?.[h]);
1128
- const parsed = codec.parse(text, {
1129
- codeHash,
1130
- pattern,
1131
- state,
1132
- Value,
1133
- isProduct,
1134
- isVariant
1135
- });
1136
- const repr = parsed instanceof Value ? parsed.toJSON() : (typeof parsed === "object" ? JSON.stringify(parsed) : String(parsed));
1137
- inputPopupStatus.innerHTML = `<span class="line-output" style="margin:0;padding:2px 6px;">✓ Valid: <code>${escapeHtml(String(repr))}</code></span>`;
1138
- } catch (err) {
1139
- inputPopupStatus.innerHTML = `<span class="line-warning" style="margin:0;padding:2px 6px;">⚠ ${escapeHtml(err.message || String(err))}</span>`;
1140
- }
1141
- }
1142
-
1143
- export async function submitInputPopup() {
1144
- const text = inputPopupText?.value ?? "";
1145
-
1146
- // If state.pendingInput is not set, configure it from the popup selections
1147
- if (!state.pendingInput) {
1148
- const rawType = inputPopupType?.value;
1149
- const codecName = inputPopupCodec?.value;
1150
- if (!rawType) {
1151
- if (inputPopupStatus) inputPopupStatus.innerHTML = `<span class="line-error">Please select a target type.</span>`;
1152
- return;
1153
- }
1154
- const codeHash = rawType === "*" ? "*" : state.typeAliases?.[rawType] || rawType;
1155
- try {
1156
- const codec = resolveCodec(state, codeHash, codecName || null, "parse");
1157
- state.pendingInput = { codeHash, codecName: codec.name, promptName: codec.name };
1158
- } catch (err) {
1159
- if (inputPopupStatus) inputPopupStatus.innerHTML = `<span class="line-error">${escapeHtml(err.message)}</span>`;
1160
- return;
1161
- }
1162
- }
1163
-
1164
- closeInputPopup();
1165
- await executeCommand(text);
1166
- }
1167
649
 
1168
650
  // Initialization on DOMContentLoaded
1169
651
  export function initRepl() {
@@ -1181,32 +663,15 @@ export function initRepl() {
1181
663
  dropOverlay = document.getElementById("drop-overlay");
1182
664
  codecsModal = document.getElementById("codecs-modal");
1183
665
  codecsBadgeEl = document.getElementById("codecs-count-badge");
1184
- activeCodecsTbody = document.getElementById("active-codecs-tbody");
1185
- codecTypeSelect = document.getElementById("codec-custom-type");
666
+ enabledCodecsGrid = document.getElementById("enabled-codecs-grid");
1186
667
 
1187
- inputPopupModal = document.getElementById("input-popup-modal");
1188
- inputPopupType = document.getElementById("input-popup-type");
1189
- inputPopupCodec = document.getElementById("input-popup-codec");
1190
- inputPopupText = document.getElementById("input-popup-text");
1191
- inputPopupStatus = document.getElementById("input-popup-status");
1192
- inputPopupHint = document.getElementById("input-popup-hint");
1193
- inputSamplesList = document.getElementById("input-samples-list");
1194
668
 
1195
669
  updatePrompt();
1196
670
 
1197
671
  // Welcome message
1198
672
  appendSystemMessage(`
1199
673
  <div class="welcome-banner">
1200
- <div class="welcome-title">λ k interactive repl</div>
1201
- <div class="welcome-desc">First-order partial functions over algebraic data types &bull; WebAssembly execution engine &bull; Custom Serializers &amp; Deserializers (:codecs)</div>
1202
- <div class="welcome-tips">
1203
- <span>💡 Try: <a href="javascript:void(0)" class="quick-link" data-code=":load core.k">:load core.k</a></span>
1204
- <span>• <a href="javascript:void(0)" class="quick-link" data-code=":codec load int">:codec load int</a></span>
1205
- <span>• <a href="javascript:void(0)" class="quick-link" data-code="10 int">10 int</a></span>
1206
- <span>• <a href="javascript:void(0)" class="quick-link" data-code="{10 int x, 5 int y} plus">{10 int x, 5 int y} plus</a></span>
1207
- <span>• <a href="javascript:void(0)" class="quick-link" data-code=":codecs">:codecs</a></span>
1208
- <span>• <a href="javascript:void(0)" class="quick-link" data-code=":help">:help</a></span>
1209
- </div>
674
+ <div class="welcome-title">k interactive repl</div>
1210
675
  </div>
1211
676
  `, false, true);
1212
677
 
@@ -1365,14 +830,6 @@ export function initRepl() {
1365
830
  const clearBtn = document.getElementById("btn-clear");
1366
831
  if (clearBtn) clearBtn.onclick = clearOutput;
1367
832
 
1368
- // Reset button
1369
- const resetBtn = document.getElementById("btn-reset");
1370
- if (resetBtn) {
1371
- resetBtn.onclick = () => {
1372
- executeCommand(":reset");
1373
- };
1374
- }
1375
-
1376
833
  // Help button
1377
834
  const helpBtn = document.getElementById("btn-help");
1378
835
  if (helpBtn) {
@@ -1408,77 +865,7 @@ export function initRepl() {
1408
865
  const closeCodecsBtn = document.getElementById("btn-close-codecs");
1409
866
  if (closeCodecsBtn) closeCodecsBtn.onclick = closeCodecsModal;
1410
867
 
1411
- // Toggle built-in codecs buttons
1412
- ["int", "utf8", "json", "ieee"].forEach(name => {
1413
- const btn = document.getElementById(`btn-toggle-codec-${name}`);
1414
- if (btn) {
1415
- btn.onclick = () => {
1416
- if (isCodecRegistered(name)) {
1417
- executeCommand(`:codec unload ${name}`);
1418
- } else {
1419
- executeCommand(`:codec load ${name}`);
1420
- }
1421
- renderCodecsModal();
1422
- };
1423
- }
1424
- });
1425
-
1426
- // Codec Studio controls
1427
- const presetSelect = document.getElementById("codec-preset-select");
1428
- if (presetSelect) {
1429
- presetSelect.addEventListener("change", () => {
1430
- if (presetSelect.value) applyCodecPreset(presetSelect.value);
1431
- });
1432
- }
1433
-
1434
- const btnRegisterCodec = document.getElementById("btn-register-codec");
1435
- if (btnRegisterCodec) btnRegisterCodec.onclick = registerCustomCodecFromForm;
1436
-
1437
- const btnTestParse = document.getElementById("btn-test-parse");
1438
- if (btnTestParse) btnTestParse.onclick = testCustomParse;
1439
-
1440
- const btnTestPrint = document.getElementById("btn-test-print");
1441
- if (btnTestPrint) btnTestPrint.onclick = testCustomPrint;
1442
-
1443
- const btnSaveCodecVfs = document.getElementById("btn-save-codec-vfs");
1444
- if (btnSaveCodecVfs) btnSaveCodecVfs.onclick = saveCustomCodecToVfs;
1445
-
1446
- // Codec Input Popup controls
1447
- const btnInputNavbar = document.getElementById("btn-input-popup");
1448
- if (btnInputNavbar) btnInputNavbar.onclick = () => openInputPopup();
1449
-
1450
- const btnCloseInput = document.getElementById("btn-close-input-popup");
1451
- if (btnCloseInput) btnCloseInput.onclick = cancelInputPopup;
1452
-
1453
- const btnCancelInput = document.getElementById("btn-cancel-input");
1454
- if (btnCancelInput) btnCancelInput.onclick = cancelInputPopup;
1455
868
 
1456
- const btnSubmitInput = document.getElementById("btn-submit-input");
1457
- if (btnSubmitInput) btnSubmitInput.onclick = submitInputPopup;
1458
-
1459
- if (inputPopupType) {
1460
- inputPopupType.addEventListener("change", updateInputPopupCodecs);
1461
- }
1462
-
1463
- if (inputPopupCodec) {
1464
- inputPopupCodec.addEventListener("change", () => {
1465
- renderInputSamples(inputPopupCodec.value);
1466
- updateLiveValidation();
1467
- });
1468
- }
1469
-
1470
- if (inputPopupText) {
1471
- inputPopupText.addEventListener("input", updateLiveValidation);
1472
- inputPopupText.addEventListener("keydown", (e) => {
1473
- if (e.key === "Enter" && (e.ctrlKey || e.metaKey || !e.shiftKey)) {
1474
- e.preventDefault();
1475
- submitInputPopup();
1476
- } else if (e.key === "Escape") {
1477
- e.preventDefault();
1478
- cancelInputPopup();
1479
- }
1480
- });
1481
- }
1482
869
 
1483
870
  // Export .klib button
1484
871
  const exportBtn = document.getElementById("btn-export");
@@ -1537,7 +924,6 @@ export function initRepl() {
1537
924
  if (e.target === vfsModal) closeVfsModal();
1538
925
  if (e.target === helpModal) helpModal.classList.remove("open");
1539
926
  if (e.target === codecsModal) closeCodecsModal();
1540
- if (e.target === inputPopupModal) cancelInputPopup();
1541
927
  });
1542
928
 
1543
929
  // Focus input when clicking anywhere in the input bar
@@ -1563,9 +949,6 @@ export function initRepl() {
1563
949
  }
1564
950
  });
1565
951
 
1566
- // Apply initial yn preset to custom form
1567
- applyCodecPreset("yn");
1568
-
1569
952
  inputEl.focus();
1570
953
  }
1571
954
 
@@ -1586,10 +969,6 @@ if (typeof window !== "undefined") {
1586
969
  setVfsFile,
1587
970
  getAllVfsFiles,
1588
971
  openCodecsModal,
1589
- openInputPopup,
1590
- closeInputPopup,
1591
- submitInputPopup,
1592
- cancelInputPopup,
1593
972
  registerCodec,
1594
973
  unregisterCodec
1595
974
  };