libfx 0.0.15 → 0.0.16
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 +8 -2
- package/browser-agent.js +21 -0
- package/browser-terminal.js +7 -0
- package/browser.js +12 -37
- package/bundled-asset.js +12 -0
- package/durable/local.mjs +3 -0
- package/durable/local.mjs.map +1 -0
- package/durable/vercel.mjs +3 -0
- package/durable/vercel.mjs.map +1 -0
- package/durable/world.mjs +3 -0
- package/durable/world.mjs.map +1 -0
- package/engine.js +2498 -0
- package/fx-core.wasm +0 -0
- package/fx-harness.js +1 -1
- package/fx-sdk.js +27 -3694
- package/fx-term.wasm +0 -0
- package/node-wasm.js +63 -0
- package/node.cjs +8 -8095
- package/node.js +94 -103
- package/package.json +10 -3
- package/types/node.d.ts +1 -1
- package/wasm-runtime.js +1049 -0
- package/wasm-terminal.js +80 -0
- package/xterm.js +112 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
package/wasm-terminal.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The fx terminal on WebAssembly.
|
|
2
|
+
import { instantiate } from "./wasm-runtime.js";
|
|
3
|
+
|
|
4
|
+
export async function createFxTerminal(options) {
|
|
5
|
+
if (!options?.terminal) throw new TypeError("terminal is required");
|
|
6
|
+
const emit = (type, detail = {}) => {
|
|
7
|
+
try { options.onEvent?.({ type, timestamp: performance.now(), ...detail }); } catch {}
|
|
8
|
+
};
|
|
9
|
+
let resolveInteractive;
|
|
10
|
+
let rejectInteractive;
|
|
11
|
+
let interactiveScheduled = false;
|
|
12
|
+
const interactive = new Promise((resolve, reject) => {
|
|
13
|
+
resolveInteractive = resolve;
|
|
14
|
+
rejectInteractive = reject;
|
|
15
|
+
});
|
|
16
|
+
const stdout = (bytes) => options.terminal.write(bytes);
|
|
17
|
+
const onTerminalPoll = () => {
|
|
18
|
+
if (interactiveScheduled) return;
|
|
19
|
+
interactiveScheduled = true;
|
|
20
|
+
queueMicrotask(async () => {
|
|
21
|
+
try {
|
|
22
|
+
await options.terminal.drain?.();
|
|
23
|
+
resolveInteractive();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
rejectInteractive(error);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
emit("runtime.start", { surface: "terminal" });
|
|
30
|
+
const runtime = await instantiate({ ...options, emit, stdout, onTerminalPoll });
|
|
31
|
+
runtime.exited.then((code) => {
|
|
32
|
+
if (!interactiveScheduled) rejectInteractive(new Error(`fx terminal exited with code ${code} before becoming interactive`));
|
|
33
|
+
});
|
|
34
|
+
emit("runtime.ready", { surface: "terminal" });
|
|
35
|
+
const interruptKey = options.interruptKey ?? "\x03";
|
|
36
|
+
const forwardData = (data) => {
|
|
37
|
+
if (interruptKey && data.includes(interruptKey)) runtime.abortHostEffects();
|
|
38
|
+
runtime.write(data);
|
|
39
|
+
};
|
|
40
|
+
const signalResize = () => {
|
|
41
|
+
emit("terminal.resize", { cols: options.terminal.cols, rows: options.terminal.rows });
|
|
42
|
+
runtime.wake();
|
|
43
|
+
};
|
|
44
|
+
let unsubscribeData;
|
|
45
|
+
let unsubscribeKeyData;
|
|
46
|
+
let unsubscribeResize;
|
|
47
|
+
let subscriptionsReleased = false;
|
|
48
|
+
const releaseSubscriptions = () => {
|
|
49
|
+
if (subscriptionsReleased) return;
|
|
50
|
+
subscriptionsReleased = true;
|
|
51
|
+
try { unsubscribeData?.(); } catch (error) { emit("terminal.cleanup_error", { source: "data", error }); }
|
|
52
|
+
try { unsubscribeKeyData?.(); } catch (error) { emit("terminal.cleanup_error", { source: "key_data", error }); }
|
|
53
|
+
try { unsubscribeResize?.(); } catch (error) { emit("terminal.cleanup_error", { source: "resize", error }); }
|
|
54
|
+
};
|
|
55
|
+
runtime.exited.then((code) => {
|
|
56
|
+
releaseSubscriptions();
|
|
57
|
+
emit("runtime.exit", { surface: "terminal", code });
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
unsubscribeData = options.terminal.onData(forwardData);
|
|
61
|
+
unsubscribeKeyData = options.terminal.onKeyData?.(forwardData);
|
|
62
|
+
unsubscribeResize = options.terminal.onResize(signalResize);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// The rejected factory never transfers this promise to a caller.
|
|
65
|
+
interactive.catch(() => {});
|
|
66
|
+
releaseSubscriptions();
|
|
67
|
+
runtime.abort();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
interactive,
|
|
72
|
+
exited: runtime.exited,
|
|
73
|
+
write(data) {
|
|
74
|
+
if (interruptKey && typeof data === "string" && data.includes(interruptKey)) runtime.abortHostEffects();
|
|
75
|
+
runtime.write(data);
|
|
76
|
+
},
|
|
77
|
+
resize: signalResize,
|
|
78
|
+
abort() { releaseSubscriptions(); runtime.abort(); },
|
|
79
|
+
};
|
|
80
|
+
}
|
package/xterm.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Connects an xterm.js Terminal to an fx terminal, native or WebAssembly.
|
|
2
|
+
const decoder = new TextDecoder();
|
|
3
|
+
|
|
4
|
+
export function encodeXtermKeyEvent(event) {
|
|
5
|
+
if (event.type !== "keydown" || event.altKey || event.ctrlKey) return null;
|
|
6
|
+
if (event.key === "Enter" && event.shiftKey && !event.metaKey) return "\x1b[13;2u";
|
|
7
|
+
if (event.metaKey) {
|
|
8
|
+
const modifiers = 8 | (event.shiftKey ? 1 : 0);
|
|
9
|
+
if (event.key === "Backspace") return `\x1b[127;${modifiers + 1}u`;
|
|
10
|
+
const arrow = { ArrowUp: "A", ArrowDown: "B", ArrowRight: "C", ArrowLeft: "D" }[event.key];
|
|
11
|
+
if (arrow) return `\x1b[1;${modifiers + 1}${arrow}`;
|
|
12
|
+
const shortcut = { a: 97, c: 99, x: 120, z: 122 }[event.key.toLowerCase()];
|
|
13
|
+
if (shortcut) return `\x1b[${shortcut};${modifiers + 1}u`;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function xtermPointerCell(term, event) {
|
|
19
|
+
const root = term.element;
|
|
20
|
+
const screen = root?.querySelector?.(".xterm-screen") || root;
|
|
21
|
+
const rect = screen?.getBoundingClientRect?.();
|
|
22
|
+
if (!rect || rect.width <= 0 || rect.height <= 0 || term.cols <= 0 || term.rows <= 0) return null;
|
|
23
|
+
if (event.clientX < rect.left || event.clientX >= rect.right ||
|
|
24
|
+
event.clientY < rect.top || event.clientY >= rect.bottom) return null;
|
|
25
|
+
return {
|
|
26
|
+
column: Math.min(term.cols, Math.floor((event.clientX - rect.left) * term.cols / rect.width) + 1),
|
|
27
|
+
row: Math.min(term.rows, Math.floor((event.clientY - rect.top) * term.rows / rect.height) + 1),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function installXtermClickHandler(term, callback) {
|
|
32
|
+
const element = term.element;
|
|
33
|
+
if (typeof element?.addEventListener !== "function") return () => {};
|
|
34
|
+
let pointerDown = null;
|
|
35
|
+
const down = (event) => {
|
|
36
|
+
if (event.button !== 0) return;
|
|
37
|
+
pointerDown = { id: event.pointerId, x: event.clientX, y: event.clientY };
|
|
38
|
+
};
|
|
39
|
+
const up = (event) => {
|
|
40
|
+
const start = pointerDown;
|
|
41
|
+
pointerDown = null;
|
|
42
|
+
if (!start || event.button !== 0 || event.pointerId !== start.id ||
|
|
43
|
+
event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
44
|
+
const dx = event.clientX - start.x;
|
|
45
|
+
const dy = event.clientY - start.y;
|
|
46
|
+
if (dx * dx + dy * dy > 16) return;
|
|
47
|
+
if (term.modes?.mouseTrackingMode && term.modes.mouseTrackingMode !== "none") return;
|
|
48
|
+
const cell = xtermPointerCell(term, event);
|
|
49
|
+
if (!cell) return;
|
|
50
|
+
callback(`\x1b[<0;${cell.column};${cell.row}M\x1b[<0;${cell.column};${cell.row}m`);
|
|
51
|
+
};
|
|
52
|
+
const cancel = () => { pointerDown = null; };
|
|
53
|
+
element.addEventListener("pointerdown", down);
|
|
54
|
+
element.addEventListener("pointerup", up);
|
|
55
|
+
element.addEventListener("pointercancel", cancel);
|
|
56
|
+
return () => {
|
|
57
|
+
element.removeEventListener("pointerdown", down);
|
|
58
|
+
element.removeEventListener("pointerup", up);
|
|
59
|
+
element.removeEventListener("pointercancel", cancel);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function installXtermShortcutHandler(term, callback) {
|
|
64
|
+
const element = term.element;
|
|
65
|
+
if (typeof element?.addEventListener !== "function") return () => {};
|
|
66
|
+
const keydown = (event) => {
|
|
67
|
+
if (xtermSelectionOwnsShortcut(term, event)) return;
|
|
68
|
+
const data = encodeXtermKeyEvent(event);
|
|
69
|
+
if (data === null) return;
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
event.stopImmediatePropagation();
|
|
72
|
+
callback(data);
|
|
73
|
+
};
|
|
74
|
+
element.addEventListener("keydown", keydown, true);
|
|
75
|
+
return () => element.removeEventListener("keydown", keydown, true);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function xtermSelectionOwnsShortcut(term, event) {
|
|
79
|
+
return event.type === "keydown" && event.metaKey &&
|
|
80
|
+
(event.key.toLowerCase() === "c" || event.key.toLowerCase() === "x") &&
|
|
81
|
+
term.hasSelection?.();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function xtermAdapter(term) {
|
|
85
|
+
let keyDataHandler = null;
|
|
86
|
+
if (typeof term.attachCustomKeyEventHandler === "function") {
|
|
87
|
+
term.attachCustomKeyEventHandler((event) => {
|
|
88
|
+
if (xtermSelectionOwnsShortcut(term, event)) return true;
|
|
89
|
+
const data = encodeXtermKeyEvent(event);
|
|
90
|
+
if (data === null || keyDataHandler === null) return true;
|
|
91
|
+
keyDataHandler(data);
|
|
92
|
+
return false;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
write(bytes) { term.write(typeof bytes === "string" ? bytes : decoder.decode(bytes)); },
|
|
97
|
+
onData(callback) { const disposable = term.onData(callback); return () => disposable.dispose(); },
|
|
98
|
+
onKeyData(callback) {
|
|
99
|
+
keyDataHandler = callback;
|
|
100
|
+
const removeShortcutHandler = installXtermShortcutHandler(term, callback);
|
|
101
|
+
const removeClickHandler = installXtermClickHandler(term, callback);
|
|
102
|
+
return () => {
|
|
103
|
+
removeShortcutHandler();
|
|
104
|
+
removeClickHandler();
|
|
105
|
+
if (keyDataHandler === callback) keyDataHandler = null;
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
get cols() { return term.cols; },
|
|
109
|
+
get rows() { return term.rows; },
|
|
110
|
+
onResize(callback) { const disposable = term.onResize(callback); return () => disposable.dispose(); },
|
|
111
|
+
};
|
|
112
|
+
}
|
package/libfx.darwin-arm64.node
DELETED
|
Binary file
|
package/libfx.darwin-x64.node
DELETED
|
Binary file
|
package/libfx.linux-arm64.node
DELETED
|
Binary file
|
package/libfx.linux-x64.node
DELETED
|
Binary file
|