@kitlangton/terminal-control-opentui 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kit Langton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @kitlangton/terminal-control-opentui
2
+
3
+ Expose a consistent semantic snapshot from any OpenTUI application controlled by Terminal Control.
4
+
5
+ ```bash
6
+ bun add @kitlangton/terminal-control-opentui @opentui/core
7
+ ```
8
+
9
+ Pass the application's live `CliRenderer` during startup:
10
+
11
+ ```ts
12
+ import { provideTerminalControl } from "@kitlangton/terminal-control-opentui"
13
+
14
+ const terminalControl = provideTerminalControl(renderer, {
15
+ application: { name: "my-tui", version: "1.0.0" },
16
+ })
17
+
18
+ renderer.once("destroy", () => terminalControl.close())
19
+ ```
20
+
21
+ Outside Terminal Control the integration is a no-op. Launch the application with Terminal Control's
22
+ OpenTUI host profile, then inspect the semantic tree:
23
+
24
+ ```bash
25
+ termctrl start app --host opentui -- my-tui
26
+ termctrl show app --format semantic
27
+ ```
28
+
29
+ The package walks the live OpenTUI render tree and consistently discovers visible focused editors,
30
+ focusable controls, and reachable mouse targets. It derives roles, labels, focus state, and live
31
+ numeric element handles without requiring every application to maintain its own walker.
32
+
33
+ `elements(renderer)` and `semanticSnapshot(renderer)` are also exported for tests and custom
34
+ integrations.
35
+
36
+ This package is released with Terminal Control's fixed-version package group. See the repository's
37
+ `docs/releasing.md` process rather than publishing this directory directly.
@@ -0,0 +1,37 @@
1
+ import type { CliRenderer } from "@opentui/core";
2
+ import { type Provider } from "./provider.js";
3
+ export type InteractableElement = {
4
+ readonly id: string;
5
+ readonly num: number;
6
+ readonly x: number;
7
+ readonly y: number;
8
+ readonly width: number;
9
+ readonly height: number;
10
+ readonly focusable: boolean;
11
+ readonly focused: boolean;
12
+ readonly clickable: boolean;
13
+ readonly editor: boolean;
14
+ };
15
+ export type SemanticNode = {
16
+ readonly id: string;
17
+ readonly role: string;
18
+ readonly label?: string;
19
+ readonly element: number;
20
+ readonly focused: boolean;
21
+ readonly disabled: boolean;
22
+ };
23
+ export type SemanticSnapshot = {
24
+ readonly format: "termctrl-semantic-snapshot-v1";
25
+ readonly nodes: ReadonlyArray<SemanticNode>;
26
+ };
27
+ export declare function elements(renderer: CliRenderer): InteractableElement[];
28
+ export declare function semanticSnapshot(renderer: CliRenderer): SemanticSnapshot;
29
+ export declare function provideTerminalControl(renderer: CliRenderer, options: {
30
+ readonly application: {
31
+ readonly name: string;
32
+ readonly version?: string;
33
+ };
34
+ readonly socketPath?: string;
35
+ readonly onError?: (error: unknown) => void;
36
+ }): Provider;
37
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,eAAe,CAAA;AAC5D,OAAO,EAA2B,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAA;AAEtE,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAA;IAChD,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;CAC5C,CAAA;AA4BD,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,mBAAmB,EAAE,CAkBrE;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,GAAG,gBAAgB,CAoBxE;AA6BD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE;IACP,QAAQ,CAAC,WAAW,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAC5C,GACA,QAAQ,CASV"}
package/dist/index.js ADDED
@@ -0,0 +1,116 @@
1
+ import { provideSemanticSnapshot } from "./provider.js";
2
+ function children(renderable) {
3
+ return renderable.getChildren().filter((child) => "num" in child);
4
+ }
5
+ function all(renderable) {
6
+ return [renderable, ...children(renderable).flatMap(all)];
7
+ }
8
+ function hasMouseListeners(renderable) {
9
+ const listener = Reflect.get(renderable, "_mouseListener");
10
+ const listeners = Reflect.get(renderable, "_mouseListeners");
11
+ return Boolean(listener) || (isRecord(listeners) && Object.keys(listeners).length > 0);
12
+ }
13
+ function receivesClick(renderer, renderable) {
14
+ const bounds = visibleBounds(renderer, renderable);
15
+ if (!bounds)
16
+ return false;
17
+ const targets = new Set(all(renderable).map((item) => item.num));
18
+ for (let y = Math.floor(bounds.y); y < Math.ceil(bounds.y + bounds.height); y++) {
19
+ for (let x = Math.floor(bounds.x); x < Math.ceil(bounds.x + bounds.width); x++) {
20
+ if (targets.has(renderer.hitTest(x, y)))
21
+ return true;
22
+ }
23
+ }
24
+ return false;
25
+ }
26
+ export function elements(renderer) {
27
+ return all(renderer.root)
28
+ .flatMap((renderable) => {
29
+ const bounds = visibleBounds(renderer, renderable);
30
+ if (!bounds)
31
+ return [];
32
+ return [
33
+ {
34
+ id: renderable.id,
35
+ num: renderable.num,
36
+ ...bounds,
37
+ focusable: renderable.focusable,
38
+ focused: renderable.focused,
39
+ clickable: hasMouseListeners(renderable) && receivesClick(renderer, renderable),
40
+ editor: renderer.currentFocusedEditor === renderable,
41
+ },
42
+ ];
43
+ })
44
+ .filter((element) => element.focusable || element.clickable || element.editor);
45
+ }
46
+ export function semanticSnapshot(renderer) {
47
+ const renderables = new Map(all(renderer.root).map((renderable) => [renderable.num, renderable]));
48
+ const ids = new Set();
49
+ return {
50
+ format: "termctrl-semantic-snapshot-v1",
51
+ nodes: elements(renderer).map((element) => {
52
+ const preferred = element.id || `renderable-${element.num}`;
53
+ let id = preferred;
54
+ for (let suffix = element.num; ids.has(id); suffix++)
55
+ id = `${preferred}-${suffix}`;
56
+ ids.add(id);
57
+ return {
58
+ id,
59
+ role: element.editor ? "textbox" : element.clickable ? "button" : "control",
60
+ label: label(renderer, renderables.get(element.num), element),
61
+ element: element.num,
62
+ focused: element.focused || element.editor,
63
+ disabled: false,
64
+ };
65
+ }),
66
+ };
67
+ }
68
+ function visibleBounds(renderer, renderable) {
69
+ if (renderable.isDestroyed ||
70
+ !renderable.visible ||
71
+ renderable.opacity <= 0 ||
72
+ renderable.width <= 0 ||
73
+ renderable.height <= 0) {
74
+ return undefined;
75
+ }
76
+ let left = Math.max(0, renderable.screenX);
77
+ let top = Math.max(0, renderable.screenY);
78
+ let right = Math.min(renderer.width, renderable.screenX + renderable.width);
79
+ let bottom = Math.min(renderer.height, renderable.screenY + renderable.height);
80
+ for (let parent = renderable.parent; parent; parent = parent.parent) {
81
+ if (parent.isDestroyed || !parent.visible || parent.opacity <= 0)
82
+ return undefined;
83
+ if (parent.overflow !== "visible") {
84
+ left = Math.max(left, parent.screenX);
85
+ top = Math.max(top, parent.screenY);
86
+ right = Math.min(right, parent.screenX + parent.width);
87
+ bottom = Math.min(bottom, parent.screenY + parent.height);
88
+ }
89
+ }
90
+ if (right <= left || bottom <= top)
91
+ return undefined;
92
+ return { x: left, y: top, width: right - left, height: bottom - top };
93
+ }
94
+ export function provideTerminalControl(renderer, options) {
95
+ return provideSemanticSnapshot({
96
+ ...options,
97
+ snapshot: async () => {
98
+ renderer.requestRender();
99
+ await renderer.idle();
100
+ return semanticSnapshot(renderer);
101
+ },
102
+ });
103
+ }
104
+ function label(renderer, renderable, element) {
105
+ if (!renderable || element.editor)
106
+ return element.id || undefined;
107
+ const text = all(renderable)
108
+ .filter((item) => visibleBounds(renderer, item) !== undefined)
109
+ .map((item) => Reflect.get(item, "plainText"))
110
+ .find((value) => typeof value === "string" && value.trim().length > 0);
111
+ return text?.replaceAll(/\s+/g, " ").trim() || element.id || undefined;
112
+ }
113
+ function isRecord(value) {
114
+ return typeof value === "object" && value !== null && !Array.isArray(value);
115
+ }
116
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export type Provider = {
2
+ readonly enabled: boolean;
3
+ readonly ready: Promise<boolean>;
4
+ close(): void;
5
+ };
6
+ export declare function provideSemanticSnapshot(options: {
7
+ readonly application: {
8
+ readonly name: string;
9
+ readonly version?: string;
10
+ };
11
+ readonly snapshot: () => unknown | Promise<unknown>;
12
+ readonly socketPath?: string;
13
+ readonly onError?: (error: unknown) => void;
14
+ }): Provider;
15
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;IAChC,KAAK,IAAI,IAAI,CAAA;CACd,CAAA;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAC/C,QAAQ,CAAC,WAAW,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1E,QAAQ,CAAC,QAAQ,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACnD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAC5C,GAAG,QAAQ,CAuHX"}
@@ -0,0 +1,121 @@
1
+ import { createConnection } from "node:net";
2
+ export function provideSemanticSnapshot(options) {
3
+ if (!options.application.name)
4
+ throw new TypeError("application.name is required");
5
+ const socketPath = options.socketPath ?? process.env.TERMCTRL_SEMANTIC_SOCKET;
6
+ if (!socketPath)
7
+ return { enabled: false, ready: Promise.resolve(false), close() { } };
8
+ const semanticSocketPath = socketPath;
9
+ let readySettled = false;
10
+ let connectedOnce = false;
11
+ let closed = false;
12
+ let socket;
13
+ let reconnectTimer;
14
+ let reconnectDelay = 25;
15
+ let resolveReady;
16
+ const ready = new Promise((resolve) => {
17
+ resolveReady = resolve;
18
+ });
19
+ const settleReady = (value) => {
20
+ if (readySettled)
21
+ return;
22
+ readySettled = true;
23
+ resolveReady(value);
24
+ };
25
+ connect();
26
+ function connect() {
27
+ let buffer = "";
28
+ let protocolReady = false;
29
+ const connection = createConnection(semanticSocketPath);
30
+ socket = connection;
31
+ connection.setEncoding("utf8");
32
+ const handshakeTimer = setTimeout(() => fail(new Error("Terminal Control semantic handshake timed out")), 5_000);
33
+ handshakeTimer.unref();
34
+ connection.once("connect", () => send({
35
+ type: "hello",
36
+ protocolVersion: 1,
37
+ application: options.application,
38
+ capabilities: ["semantic.snapshot"],
39
+ }));
40
+ connection.on("data", (chunk) => {
41
+ buffer += chunk;
42
+ const lines = buffer.split("\n");
43
+ buffer = lines.pop() ?? "";
44
+ for (const line of lines) {
45
+ if (!line)
46
+ return fail(new Error("Terminal Control sent an empty semantic message"));
47
+ try {
48
+ receive(JSON.parse(line));
49
+ }
50
+ catch (error) {
51
+ return fail(error);
52
+ }
53
+ }
54
+ });
55
+ connection.once("error", fail);
56
+ connection.once("close", () => {
57
+ clearTimeout(handshakeTimer);
58
+ if (socket === connection)
59
+ socket = undefined;
60
+ settleReady(false);
61
+ if (!closed && connectedOnce) {
62
+ reconnectTimer = setTimeout(connect, reconnectDelay);
63
+ reconnectTimer.unref();
64
+ reconnectDelay = Math.min(reconnectDelay * 2, 1_000);
65
+ }
66
+ });
67
+ function receive(message) {
68
+ if (!isRecord(message))
69
+ throw new Error("Terminal Control sent an invalid semantic message");
70
+ if (message.type === "ready" && message.protocolVersion === 1 && !protocolReady) {
71
+ protocolReady = true;
72
+ connectedOnce = true;
73
+ reconnectDelay = 25;
74
+ clearTimeout(handshakeTimer);
75
+ settleReady(true);
76
+ return;
77
+ }
78
+ if (message.type !== "semantic.snapshot" || !protocolReady || !Number.isSafeInteger(message.id)) {
79
+ throw new Error("Terminal Control sent an invalid semantic snapshot request");
80
+ }
81
+ const id = message.id;
82
+ Promise.resolve()
83
+ .then(options.snapshot)
84
+ .then((value) => send({ type: "result", id, value }))
85
+ .catch((error) => send({
86
+ type: "error",
87
+ id,
88
+ error: {
89
+ code: isRecord(error) && typeof error.code === "string" ? error.code : "SNAPSHOT_FAILED",
90
+ message: error instanceof Error ? error.message : String(error),
91
+ },
92
+ }));
93
+ }
94
+ function send(message) {
95
+ if (!connection.destroyed)
96
+ connection.write(`${JSON.stringify(message)}\n`);
97
+ }
98
+ function fail(error) {
99
+ clearTimeout(handshakeTimer);
100
+ if (!connectedOnce)
101
+ settleReady(false);
102
+ options.onError?.(error);
103
+ connection.destroy();
104
+ }
105
+ }
106
+ return {
107
+ enabled: true,
108
+ ready,
109
+ close() {
110
+ closed = true;
111
+ if (reconnectTimer)
112
+ clearTimeout(reconnectTimer);
113
+ settleReady(false);
114
+ socket?.destroy();
115
+ },
116
+ };
117
+ }
118
+ function isRecord(value) {
119
+ return typeof value === "object" && value !== null && !Array.isArray(value);
120
+ }
121
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@kitlangton/terminal-control-opentui",
3
+ "version": "0.6.0",
4
+ "description": "Expose consistent OpenTUI semantic snapshots through Terminal Control",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/anomalyco/terminal-control.git",
9
+ "directory": "packages/opentui"
10
+ },
11
+ "type": "module",
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "types": "./dist/index.d.ts",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "scripts": {
31
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
32
+ "test": "bun test src/index.test.ts",
33
+ "typecheck": "tsc --noEmit",
34
+ "prepack": "npm run build",
35
+ "release": "node script/release.mjs",
36
+ "release:check": "node script/release.mjs --check"
37
+ },
38
+ "peerDependencies": {
39
+ "@opentui/core": ">=0.4.1 <0.5.0"
40
+ },
41
+ "devDependencies": {
42
+ "@opentui/core": "^0.4.5",
43
+ "@types/bun": "^1.3.0",
44
+ "@types/node": "^24.0.0",
45
+ "typescript": "^5.9.0"
46
+ }
47
+ }