@flowtty/core 1.0.0-alpha.1
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 +35 -0
- package/dist/backend-BcB7VR87.d.ts +74 -0
- package/dist/cells-CaXEx4lH.d.ts +31 -0
- package/dist/chunk-D6HW2BNM.js +106 -0
- package/dist/host/index.d.ts +8 -0
- package/dist/host/index.js +357 -0
- package/dist/host-DttBG-OZ.d.ts +151 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +382 -0
- package/dist/testing/index.d.ts +63 -0
- package/dist/testing/index.js +72 -0
- package/package.json +53 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { B as Buffer } from '../cells-CaXEx4lH.js';
|
|
2
|
+
import { B as Backend, K as Key } from '../backend-BcB7VR87.js';
|
|
3
|
+
|
|
4
|
+
declare class TestBackend implements Backend {
|
|
5
|
+
private readonly cols;
|
|
6
|
+
private readonly rows;
|
|
7
|
+
frames: string[];
|
|
8
|
+
private buffers;
|
|
9
|
+
private readonly subscribers;
|
|
10
|
+
constructor(cols?: number, rows?: number);
|
|
11
|
+
size(): {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
draw(buffer: Buffer): void;
|
|
16
|
+
get lastFrame(): string;
|
|
17
|
+
get lastBuffer(): Buffer | null;
|
|
18
|
+
onKey(handler: (key: Key) => void): () => void;
|
|
19
|
+
/** Synchronously deliver one Key to every subscriber. */
|
|
20
|
+
press(key: Partial<Key> & {
|
|
21
|
+
name: string;
|
|
22
|
+
}): void;
|
|
23
|
+
/** Emit one Key per character; printable chars only. */
|
|
24
|
+
type(text: string): void;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve after pending microtasks have drained. Use after `backend.press(...)`
|
|
30
|
+
* to wait for React's state update + the scheduled repaint:
|
|
31
|
+
*
|
|
32
|
+
* backend.press({ name: 'a' });
|
|
33
|
+
* await flush();
|
|
34
|
+
* expect(backend.lastFrame).toBe('a');
|
|
35
|
+
*/
|
|
36
|
+
declare function flush(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Wait for React scheduler-driven re-renders triggered by `setState` inside
|
|
39
|
+
* `useEffect` (e.g. a Form field registering, then the group auto-focusing the
|
|
40
|
+
* first field) to fully commit.
|
|
41
|
+
*
|
|
42
|
+
* A passive-effect `setState` lands on React's default lane, which the
|
|
43
|
+
* production Scheduler only drains on a macrotask — there is no synchronous
|
|
44
|
+
* escape hatch in react-reconciler. A single `setTimeout(0)` (the old impl)
|
|
45
|
+
* therefore advances only ONE step of a multi-step effect cascade and can also
|
|
46
|
+
* lose the race against the Scheduler's own MessageChannel macrotask; both
|
|
47
|
+
* surface as a stale/empty `lastFrame`.
|
|
48
|
+
*
|
|
49
|
+
* Passing the `TestBackend` makes this deterministic: each round yields one
|
|
50
|
+
* macrotask (draining the Scheduler, which flushes that commit's passive
|
|
51
|
+
* effects exactly as in production) plus a microtask pair (for the coalesced
|
|
52
|
+
* repaint), and we stop once two consecutive rounds add no new frame. Two
|
|
53
|
+
* rounds — not one — so a single macrotask-ordering inversion (Scheduler work
|
|
54
|
+
* landing just after our `setTimeout`) can't read as premature quiescence.
|
|
55
|
+
*
|
|
56
|
+
* Called with no backend it falls back to a single macrotask round (the legacy
|
|
57
|
+
* behavior) for callers that don't need cascade-settling.
|
|
58
|
+
*/
|
|
59
|
+
declare function flushAsync(backend?: {
|
|
60
|
+
readonly frames: readonly unknown[];
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
|
|
63
|
+
export { TestBackend, flush, flushAsync };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/testing/test-backend.ts
|
|
2
|
+
var TestBackend = class {
|
|
3
|
+
constructor(cols = 40, rows = 10) {
|
|
4
|
+
this.cols = cols;
|
|
5
|
+
this.rows = rows;
|
|
6
|
+
}
|
|
7
|
+
cols;
|
|
8
|
+
rows;
|
|
9
|
+
frames = [];
|
|
10
|
+
buffers = [];
|
|
11
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
12
|
+
size() {
|
|
13
|
+
return { width: this.cols, height: this.rows };
|
|
14
|
+
}
|
|
15
|
+
draw(buffer) {
|
|
16
|
+
this.frames.push(buffer.toString());
|
|
17
|
+
this.buffers.push(buffer);
|
|
18
|
+
}
|
|
19
|
+
get lastFrame() {
|
|
20
|
+
return this.frames[this.frames.length - 1] ?? "";
|
|
21
|
+
}
|
|
22
|
+
get lastBuffer() {
|
|
23
|
+
return this.buffers[this.buffers.length - 1] ?? null;
|
|
24
|
+
}
|
|
25
|
+
onKey(handler) {
|
|
26
|
+
this.subscribers.add(handler);
|
|
27
|
+
return () => {
|
|
28
|
+
this.subscribers.delete(handler);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Synchronously deliver one Key to every subscriber. */
|
|
32
|
+
press(key) {
|
|
33
|
+
const k = {
|
|
34
|
+
sequence: key.sequence ?? "",
|
|
35
|
+
ctrl: key.ctrl ?? false,
|
|
36
|
+
meta: key.meta ?? false,
|
|
37
|
+
shift: key.shift ?? false,
|
|
38
|
+
name: key.name
|
|
39
|
+
};
|
|
40
|
+
for (const h of [...this.subscribers]) h(k);
|
|
41
|
+
}
|
|
42
|
+
/** Emit one Key per character; printable chars only. */
|
|
43
|
+
type(text) {
|
|
44
|
+
for (const ch of text) this.press({ name: ch, sequence: ch });
|
|
45
|
+
}
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
47
|
+
dispose() {
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/testing/index.ts
|
|
52
|
+
async function flush() {
|
|
53
|
+
await Promise.resolve();
|
|
54
|
+
await Promise.resolve();
|
|
55
|
+
}
|
|
56
|
+
async function flushAsync(backend) {
|
|
57
|
+
if (!backend) {
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const MAX_ROUNDS = 20;
|
|
62
|
+
let stableRounds = 0;
|
|
63
|
+
for (let i = 0; i < MAX_ROUNDS && stableRounds < 2; i++) {
|
|
64
|
+
const before = backend.frames.length;
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
66
|
+
await Promise.resolve();
|
|
67
|
+
await Promise.resolve();
|
|
68
|
+
stableRounds = backend.frames.length === before ? stableRounds + 1 : 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { TestBackend, flush, flushAsync };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flowtty/core",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mellonis/flowtty.git",
|
|
9
|
+
"directory": "packages/core"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/mellonis/flowtty#readme",
|
|
12
|
+
"bugs": "https://github.com/mellonis/flowtty/issues",
|
|
13
|
+
"description": "Framework-free core: Buffer, Cell, Style, Key, Backend interface. Every adapter (React, Svelte) and every backend (TTY, Test, Electron) depends on this.",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"flowtty",
|
|
16
|
+
"terminal",
|
|
17
|
+
"tui",
|
|
18
|
+
"terminal-ui",
|
|
19
|
+
"cli",
|
|
20
|
+
"react-reconciler",
|
|
21
|
+
"yoga",
|
|
22
|
+
"flexbox",
|
|
23
|
+
"ansi",
|
|
24
|
+
"buffer"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./host": {
|
|
32
|
+
"types": "./dist/host/index.d.ts",
|
|
33
|
+
"import": "./dist/host/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./testing": {
|
|
36
|
+
"types": "./dist/testing/index.d.ts",
|
|
37
|
+
"import": "./dist/testing/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"prepublishOnly": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"yoga-layout": "^3.2.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"tsup": "^8"
|
|
52
|
+
}
|
|
53
|
+
}
|