@h-rig/cli 0.0.6-alpha.8 → 0.0.6-alpha.81
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 +1 -1
- package/dist/bin/build-rig-binaries.d.ts +2 -0
- package/dist/bin/rig.d.ts +2 -0
- package/dist/bin/rig.js +18374 -8205
- package/dist/src/app/board.d.ts +23 -0
- package/dist/src/app/board.js +1815 -0
- package/dist/src/app/drone-ui.d.ts +37 -0
- package/dist/src/app/drone-ui.js +294 -0
- package/dist/src/app/theme.d.ts +47 -0
- package/dist/src/app/theme.js +151 -0
- package/dist/src/app-opentui/adapters/common.d.ts +53 -0
- package/dist/src/app-opentui/adapters/common.js +149 -0
- package/dist/src/app-opentui/adapters/doctor.d.ts +11 -0
- package/dist/src/app-opentui/adapters/doctor.js +780 -0
- package/dist/src/app-opentui/adapters/fleet.d.ts +16 -0
- package/dist/src/app-opentui/adapters/fleet.js +874 -0
- package/dist/src/app-opentui/adapters/inbox.d.ts +33 -0
- package/dist/src/app-opentui/adapters/inbox.js +1454 -0
- package/dist/src/app-opentui/adapters/init.d.ts +13 -0
- package/dist/src/app-opentui/adapters/init.js +2357 -0
- package/dist/src/app-opentui/adapters/pi-attach.d.ts +16 -0
- package/dist/src/app-opentui/adapters/pi-attach.js +1295 -0
- package/dist/src/app-opentui/adapters/run-detail.d.ts +20 -0
- package/dist/src/app-opentui/adapters/run-detail.js +893 -0
- package/dist/src/app-opentui/adapters/server.d.ts +14 -0
- package/dist/src/app-opentui/adapters/server.js +798 -0
- package/dist/src/app-opentui/adapters/tasks.d.ts +55 -0
- package/dist/src/app-opentui/adapters/tasks.js +3472 -0
- package/dist/src/app-opentui/bootstrap.d.ts +16 -0
- package/dist/src/app-opentui/bootstrap.js +19509 -0
- package/dist/src/app-opentui/drone.d.ts +12 -0
- package/dist/src/app-opentui/drone.js +227 -0
- package/dist/src/app-opentui/events.d.ts +7 -0
- package/dist/src/app-opentui/events.js +28 -0
- package/dist/src/app-opentui/index.d.ts +8 -0
- package/dist/src/app-opentui/index.js +3477 -0
- package/dist/src/app-opentui/intent.d.ts +3 -0
- package/dist/src/app-opentui/intent.js +211 -0
- package/dist/src/app-opentui/layout.d.ts +15 -0
- package/dist/src/app-opentui/layout.js +44 -0
- package/dist/src/app-opentui/list-search.d.ts +8 -0
- package/dist/src/app-opentui/list-search.js +43 -0
- package/dist/src/app-opentui/pi-host-child.d.ts +2 -0
- package/dist/src/app-opentui/pi-host-child.js +778 -0
- package/dist/src/app-opentui/pi-pty-host.d.ts +64 -0
- package/dist/src/app-opentui/pi-pty-host.js +384 -0
- package/dist/src/app-opentui/registry.d.ts +4 -0
- package/dist/src/app-opentui/registry.js +6835 -0
- package/dist/src/app-opentui/render/graphics.d.ts +39 -0
- package/dist/src/app-opentui/render/graphics.js +537 -0
- package/dist/src/app-opentui/render/image-visual-layer-native-canvas.d.ts +2 -0
- package/dist/src/app-opentui/render/image-visual-layer-native-canvas.js +1480 -0
- package/dist/src/app-opentui/render/image-visual-layer-worker.d.ts +1 -0
- package/dist/src/app-opentui/render/image-visual-layer-worker.js +1541 -0
- package/dist/src/app-opentui/render/image-visual-layer.d.ts +93 -0
- package/dist/src/app-opentui/render/image-visual-layer.js +945 -0
- package/dist/src/app-opentui/render/panels.d.ts +10 -0
- package/dist/src/app-opentui/render/panels.js +201 -0
- package/dist/src/app-opentui/render/scene.d.ts +16 -0
- package/dist/src/app-opentui/render/scene.js +110 -0
- package/dist/src/app-opentui/render/text.d.ts +10 -0
- package/dist/src/app-opentui/render/text.js +121 -0
- package/dist/src/app-opentui/render/type-bar.d.ts +14 -0
- package/dist/src/app-opentui/render/type-bar.js +137 -0
- package/dist/src/app-opentui/runtime.d.ts +2 -0
- package/dist/src/app-opentui/runtime.js +3406 -0
- package/dist/src/app-opentui/scenes/doctor.d.ts +2 -0
- package/dist/src/app-opentui/scenes/doctor.js +91 -0
- package/dist/src/app-opentui/scenes/error.d.ts +2 -0
- package/dist/src/app-opentui/scenes/error.js +190 -0
- package/dist/src/app-opentui/scenes/fleet.d.ts +3 -0
- package/dist/src/app-opentui/scenes/fleet.js +207 -0
- package/dist/src/app-opentui/scenes/handoff.d.ts +2 -0
- package/dist/src/app-opentui/scenes/handoff.js +147 -0
- package/dist/src/app-opentui/scenes/help.d.ts +3 -0
- package/dist/src/app-opentui/scenes/help.js +138 -0
- package/dist/src/app-opentui/scenes/inbox.d.ts +2 -0
- package/dist/src/app-opentui/scenes/inbox.js +118 -0
- package/dist/src/app-opentui/scenes/init.d.ts +2 -0
- package/dist/src/app-opentui/scenes/init.js +94 -0
- package/dist/src/app-opentui/scenes/main.d.ts +2 -0
- package/dist/src/app-opentui/scenes/main.js +96 -0
- package/dist/src/app-opentui/scenes/run-detail.d.ts +2 -0
- package/dist/src/app-opentui/scenes/run-detail.js +139 -0
- package/dist/src/app-opentui/scenes/server.d.ts +2 -0
- package/dist/src/app-opentui/scenes/server.js +88 -0
- package/dist/src/app-opentui/scenes/tasks.d.ts +3 -0
- package/dist/src/app-opentui/scenes/tasks.js +230 -0
- package/dist/src/app-opentui/state.d.ts +4 -0
- package/dist/src/app-opentui/state.js +286 -0
- package/dist/src/app-opentui/theme.d.ts +36 -0
- package/dist/src/app-opentui/theme.js +88 -0
- package/dist/src/app-opentui/types.d.ts +188 -0
- package/dist/src/app-opentui/types.js +1 -0
- package/dist/src/commands/_async-ui.d.ts +13 -0
- package/dist/src/commands/_async-ui.js +223 -0
- package/dist/src/commands/_authority-runs.d.ts +22 -0
- package/dist/src/commands/_authority-runs.js +2 -3
- package/dist/src/commands/_cli-format.d.ts +49 -0
- package/dist/src/commands/_cli-format.js +447 -0
- package/dist/src/commands/_connection-state.d.ts +44 -0
- package/dist/src/commands/_connection-state.js +30 -11
- package/dist/src/commands/_doctor-checks.d.ts +52 -0
- package/dist/src/commands/_doctor-checks.js +180 -44
- package/dist/src/commands/_help-catalog.d.ts +51 -0
- package/dist/src/commands/_help-catalog.js +442 -0
- package/dist/src/commands/_json-output.d.ts +11 -0
- package/dist/src/commands/_json-output.js +56 -0
- package/dist/src/commands/_operator-surface.d.ts +34 -0
- package/dist/src/commands/_operator-surface.js +220 -0
- package/dist/src/commands/_operator-view.d.ts +30 -0
- package/dist/src/commands/_operator-view.js +803 -73
- package/dist/src/commands/_parsers.d.ts +15 -0
- package/dist/src/commands/_parsers.js +18 -11
- package/dist/src/commands/_paths.d.ts +11 -0
- package/dist/src/commands/_pi-frontend.d.ts +27 -0
- package/dist/src/commands/_pi-frontend.js +742 -0
- package/dist/src/commands/_pi-install.d.ts +42 -0
- package/dist/src/commands/_pi-install.js +5 -4
- package/dist/src/commands/_policy.d.ts +8 -0
- package/dist/src/commands/_policy.js +12 -5
- package/dist/src/commands/_preflight.d.ts +22 -0
- package/dist/src/commands/_preflight.js +190 -128
- package/dist/src/commands/_probes.d.ts +1 -0
- package/dist/src/commands/_run-driver-helpers.d.ts +99 -0
- package/dist/src/commands/_run-driver-helpers.js +75 -22
- package/dist/src/commands/_run-replay.d.ts +24 -0
- package/dist/src/commands/_run-replay.js +142 -0
- package/dist/src/commands/_server-client.d.ts +186 -0
- package/dist/src/commands/_server-client.js +360 -61
- package/dist/src/commands/_snapshot-upload.d.ts +39 -0
- package/dist/src/commands/_snapshot-upload.js +163 -39
- package/dist/src/commands/_spinner.d.ts +25 -0
- package/dist/src/commands/_spinner.js +65 -0
- package/dist/src/commands/_task-picker.d.ts +9 -0
- package/dist/src/commands/_task-picker.js +172 -19
- package/dist/src/commands/agent.d.ts +3 -0
- package/dist/src/commands/agent.js +39 -20
- package/dist/src/commands/browser.d.ts +65 -0
- package/dist/src/commands/browser.js +334 -51
- package/dist/src/commands/connect.d.ts +7 -0
- package/dist/src/commands/connect.js +272 -33
- package/dist/src/commands/dist.d.ts +28 -0
- package/dist/src/commands/dist.js +19 -12
- package/dist/src/commands/doctor.d.ts +3 -0
- package/dist/src/commands/doctor.js +378 -45
- package/dist/src/commands/github.d.ts +3 -0
- package/dist/src/commands/github.js +375 -53
- package/dist/src/commands/inbox.d.ts +30 -0
- package/dist/src/commands/inbox.js +760 -70
- package/dist/src/commands/init.d.ts +74 -0
- package/dist/src/commands/init.js +963 -119
- package/dist/src/commands/inspect.d.ts +3 -0
- package/dist/src/commands/inspect.js +588 -32
- package/dist/src/commands/inspector.d.ts +3 -0
- package/dist/src/commands/inspector.js +20 -13
- package/dist/src/commands/pi.d.ts +3 -0
- package/dist/src/commands/pi.js +177 -0
- package/dist/src/commands/plugin.d.ts +16 -0
- package/dist/src/commands/plugin.js +95 -27
- package/dist/src/commands/profile-and-review.d.ts +4 -0
- package/dist/src/commands/profile-and-review.js +26 -19
- package/dist/src/commands/queue.d.ts +3 -0
- package/dist/src/commands/queue.js +32 -12
- package/dist/src/commands/remote.d.ts +3 -0
- package/dist/src/commands/remote.js +43 -36
- package/dist/src/commands/repo-git-harness.d.ts +5 -0
- package/dist/src/commands/repo-git-harness.js +22 -15
- package/dist/src/commands/run.d.ts +3 -0
- package/dist/src/commands/run.js +1379 -159
- package/dist/src/commands/server.d.ts +7 -0
- package/dist/src/commands/server.js +502 -57
- package/dist/src/commands/setup.d.ts +16 -0
- package/dist/src/commands/setup.js +390 -63
- package/dist/src/commands/stats.d.ts +12 -0
- package/dist/src/commands/stats.js +1051 -0
- package/dist/src/commands/task-report-bug.d.ts +19 -0
- package/dist/src/commands/task-report-bug.js +260 -62
- package/dist/src/commands/task-run-driver.d.ts +132 -0
- package/dist/src/commands/task-run-driver.js +886 -130
- package/dist/src/commands/task.d.ts +14 -0
- package/dist/src/commands/task.js +1644 -314
- package/dist/src/commands/test.d.ts +3 -0
- package/dist/src/commands/test.js +15 -8
- package/dist/src/commands/workspace.d.ts +3 -0
- package/dist/src/commands/workspace.js +18 -11
- package/dist/src/commands.d.ts +29 -0
- package/dist/src/commands.js +10996 -6637
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +10197 -5787
- package/dist/src/launcher.d.ts +61 -0
- package/dist/src/launcher.js +77 -13
- package/dist/src/report-bug.d.ts +44 -0
- package/dist/src/report-bug.js +3 -3
- package/dist/src/runner.d.ts +47 -0
- package/dist/src/runner.js +16 -22
- package/dist/src/withMutedConsole.d.ts +2 -0
- package/package.json +16 -6
|
@@ -0,0 +1,3406 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli/src/app-opentui/runtime.ts
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
import { createCliRenderer } from "@opentui/core";
|
|
6
|
+
|
|
7
|
+
// packages/cli/src/app-opentui/events.ts
|
|
8
|
+
function createAppEventBus() {
|
|
9
|
+
const listeners = new Set;
|
|
10
|
+
return {
|
|
11
|
+
emit(event) {
|
|
12
|
+
for (const listener of [...listeners])
|
|
13
|
+
listener(event);
|
|
14
|
+
},
|
|
15
|
+
subscribe(listener) {
|
|
16
|
+
listeners.add(listener);
|
|
17
|
+
return () => listeners.delete(listener);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function normalizeError(error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
const rawHint = error instanceof Error ? error.hint : undefined;
|
|
24
|
+
return {
|
|
25
|
+
message: message || "Unknown Rig app error",
|
|
26
|
+
...typeof rawHint === "string" && rawHint.trim() ? { hint: rawHint.trim() } : {},
|
|
27
|
+
cause: error
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// packages/cli/src/app-opentui/intent.ts
|
|
32
|
+
var NAV_SCENES = new Map([
|
|
33
|
+
["", "fleet"],
|
|
34
|
+
["main", "fleet"],
|
|
35
|
+
["home", "fleet"],
|
|
36
|
+
["fleet", "fleet"],
|
|
37
|
+
["runs", "fleet"],
|
|
38
|
+
["run", "fleet"],
|
|
39
|
+
["tasks", "tasks"],
|
|
40
|
+
["task", "tasks"],
|
|
41
|
+
["help", "help"]
|
|
42
|
+
]);
|
|
43
|
+
var TASK_VIEW_COMMANDS = new Set([
|
|
44
|
+
"",
|
|
45
|
+
"all",
|
|
46
|
+
"open",
|
|
47
|
+
"ready",
|
|
48
|
+
"blocked",
|
|
49
|
+
"closed",
|
|
50
|
+
"done",
|
|
51
|
+
"mine",
|
|
52
|
+
"me",
|
|
53
|
+
"sort",
|
|
54
|
+
"status",
|
|
55
|
+
"state",
|
|
56
|
+
"assignee",
|
|
57
|
+
"assigned",
|
|
58
|
+
"label",
|
|
59
|
+
"search",
|
|
60
|
+
"q"
|
|
61
|
+
]);
|
|
62
|
+
function isTaskViewCommand(command) {
|
|
63
|
+
return !command || TASK_VIEW_COMMANDS.has(command) || command.startsWith("-");
|
|
64
|
+
}
|
|
65
|
+
function intent(scene, argv, kind, payload, label, raw) {
|
|
66
|
+
return {
|
|
67
|
+
scene,
|
|
68
|
+
argv,
|
|
69
|
+
...raw ? { raw } : {},
|
|
70
|
+
action: {
|
|
71
|
+
kind,
|
|
72
|
+
...label ? { label } : {},
|
|
73
|
+
...payload ? { payload } : {}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function unsupportedPlainCliIntent(argv, raw) {
|
|
78
|
+
const command = argv.join(" ").trim() || "command";
|
|
79
|
+
return intent("help", argv, "none", { unsupported: command }, `${command} is on the plain CLI path; exit Rig shell or rerun with RIG_PLAIN=1`, raw);
|
|
80
|
+
}
|
|
81
|
+
function firstValueAfter(args, option) {
|
|
82
|
+
const index = args.indexOf(option);
|
|
83
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
84
|
+
}
|
|
85
|
+
function firstNonOption(args) {
|
|
86
|
+
return args.find((part) => part.trim() && !part.startsWith("-"));
|
|
87
|
+
}
|
|
88
|
+
function explicitTaskIdArg(args) {
|
|
89
|
+
const taskValue = firstValueAfter(args, "--task");
|
|
90
|
+
if (taskValue && !taskValue.startsWith("-"))
|
|
91
|
+
return taskValue;
|
|
92
|
+
return args.find((part) => part.trim() && part !== "next" && part !== "--next" && part !== "--task" && !part.startsWith("-"));
|
|
93
|
+
}
|
|
94
|
+
function taskViewPayload(command, rest) {
|
|
95
|
+
const normalized = command.toLowerCase();
|
|
96
|
+
const args = command ? [command, ...rest] : rest;
|
|
97
|
+
const sort = firstValueAfter(args, "--sort") ?? firstValueAfter(args, "-s");
|
|
98
|
+
const state = firstValueAfter(args, "--state");
|
|
99
|
+
const status = firstValueAfter(args, "--status");
|
|
100
|
+
const assignee = firstValueAfter(args, "--assignee") ?? firstValueAfter(args, "--assigned");
|
|
101
|
+
const label = firstValueAfter(args, "--label");
|
|
102
|
+
const search = firstValueAfter(args, "--search") ?? firstValueAfter(args, "-q");
|
|
103
|
+
const optionPayload = {
|
|
104
|
+
...sort ? { taskSort: sort } : {},
|
|
105
|
+
...state ? { taskState: state, taskFilterPreset: "custom" } : {},
|
|
106
|
+
...status ? { taskStatus: status, taskFilterPreset: "custom" } : {},
|
|
107
|
+
...assignee ? { taskAssignee: assignee, taskFilterPreset: "custom" } : {},
|
|
108
|
+
...label ? { taskLabel: label, taskFilterPreset: "custom" } : {},
|
|
109
|
+
...search ? { taskSearch: search, taskFilterPreset: "custom" } : {}
|
|
110
|
+
};
|
|
111
|
+
if (normalized === "sort") {
|
|
112
|
+
const value = firstNonOption(rest) ?? sort ?? "priority";
|
|
113
|
+
return { payload: { ...optionPayload, taskSort: value }, label: `Sorting tasks by ${value}` };
|
|
114
|
+
}
|
|
115
|
+
if (normalized === "status") {
|
|
116
|
+
const value = firstNonOption(rest) ?? status ?? "open";
|
|
117
|
+
return { payload: { ...optionPayload, taskStatus: value, taskFilterPreset: "custom" }, label: `Filtering tasks by status ${value}` };
|
|
118
|
+
}
|
|
119
|
+
if (normalized === "state") {
|
|
120
|
+
const value = firstNonOption(rest) ?? state ?? "open";
|
|
121
|
+
return { payload: { ...optionPayload, taskState: value, taskFilterPreset: "custom" }, label: `Filtering tasks by state ${value}` };
|
|
122
|
+
}
|
|
123
|
+
if (normalized === "assignee" || normalized === "assigned" || normalized === "mine" || normalized === "me") {
|
|
124
|
+
const value = normalized === "mine" || normalized === "me" ? "me" : firstNonOption(rest) ?? assignee ?? "me";
|
|
125
|
+
return { payload: { ...optionPayload, taskAssignee: value, taskState: "open", taskFilterPreset: value === "me" ? "mine" : "custom" }, label: value === "me" ? "Filtering my tasks" : `Filtering tasks for ${value}` };
|
|
126
|
+
}
|
|
127
|
+
if (normalized === "label") {
|
|
128
|
+
const value = firstNonOption(rest) ?? label;
|
|
129
|
+
return { payload: { ...optionPayload, ...value ? { taskLabel: value } : {}, taskFilterPreset: "custom" }, label: value ? `Filtering tasks labeled ${value}` : "Filtering tasks by label" };
|
|
130
|
+
}
|
|
131
|
+
if (normalized === "search" || normalized === "q") {
|
|
132
|
+
const value = rest.join(" ").trim() || search;
|
|
133
|
+
return { payload: { ...optionPayload, ...value ? { taskSearch: value } : {}, taskFilterPreset: "custom" }, label: value ? `Searching tasks for ${value}` : "Searching tasks" };
|
|
134
|
+
}
|
|
135
|
+
if (["all", "open", "ready", "blocked", "closed", "done"].includes(normalized)) {
|
|
136
|
+
return { payload: { ...optionPayload, taskFilterPreset: normalized === "done" ? "closed" : normalized }, label: `Filtering tasks: ${normalized}` };
|
|
137
|
+
}
|
|
138
|
+
if (Object.keys(optionPayload).length > 0)
|
|
139
|
+
return { payload: optionPayload, label: "Filtering tasks" };
|
|
140
|
+
return { label: "Loading tasks" };
|
|
141
|
+
}
|
|
142
|
+
function taskViewIntent(argv, command = "", rest = [], raw) {
|
|
143
|
+
const { payload, label } = taskViewPayload(command, rest);
|
|
144
|
+
return intent("tasks", argv, "refresh", payload, label, raw);
|
|
145
|
+
}
|
|
146
|
+
function intentFromArgv(argv) {
|
|
147
|
+
const [group = "", command = "", ...rest] = argv;
|
|
148
|
+
const normalizedGroup = group.toLowerCase();
|
|
149
|
+
const normalizedCommand = command.toLowerCase();
|
|
150
|
+
if (!normalizedGroup)
|
|
151
|
+
return intent("fleet", argv, "refresh", undefined, "Opening runs");
|
|
152
|
+
if (normalizedGroup === "--help" || normalizedGroup === "-h" || normalizedGroup === "help") {
|
|
153
|
+
return argv.length === 1 ? intent("help", argv, "none", undefined, "Help") : unsupportedPlainCliIntent(argv);
|
|
154
|
+
}
|
|
155
|
+
if (normalizedGroup === "task") {
|
|
156
|
+
if (normalizedCommand === "run") {
|
|
157
|
+
if (rest.includes("--next") || rest.includes("next")) {
|
|
158
|
+
return intent("tasks", argv, "task-run-next", undefined, "Dispatching next task");
|
|
159
|
+
}
|
|
160
|
+
const task = explicitTaskIdArg(rest);
|
|
161
|
+
if (task)
|
|
162
|
+
return intent("tasks", argv, "task-run-id", { task }, `Dispatching ${task}`);
|
|
163
|
+
return intent("tasks", argv, "refresh", undefined, "Select a task, then press Enter; or use run next");
|
|
164
|
+
}
|
|
165
|
+
if (!isTaskViewCommand(normalizedCommand))
|
|
166
|
+
return unsupportedPlainCliIntent(argv);
|
|
167
|
+
return taskViewIntent(argv, normalizedCommand, rest);
|
|
168
|
+
}
|
|
169
|
+
if (normalizedGroup === "tasks") {
|
|
170
|
+
if (!isTaskViewCommand(normalizedCommand))
|
|
171
|
+
return unsupportedPlainCliIntent(argv);
|
|
172
|
+
return taskViewIntent(argv, normalizedCommand, rest);
|
|
173
|
+
}
|
|
174
|
+
if (normalizedGroup === "run") {
|
|
175
|
+
if (normalizedCommand === "attach") {
|
|
176
|
+
const runId = firstNonOption(rest);
|
|
177
|
+
return intent("handoff", argv, "run-attach", { runId }, runId ? `Attaching ${runId}` : "Attaching run");
|
|
178
|
+
}
|
|
179
|
+
if (!normalizedCommand)
|
|
180
|
+
return intent("fleet", argv, "refresh", undefined, "Loading fleet");
|
|
181
|
+
return unsupportedPlainCliIntent(argv);
|
|
182
|
+
}
|
|
183
|
+
if (normalizedGroup === "attach") {
|
|
184
|
+
const runId = firstNonOption([command, ...rest]);
|
|
185
|
+
return intent("handoff", argv, "run-attach", { runId }, runId ? `Attaching ${runId}` : "Attaching run");
|
|
186
|
+
}
|
|
187
|
+
const nav = NAV_SCENES.get(normalizedGroup);
|
|
188
|
+
if (nav)
|
|
189
|
+
return intent(nav, argv, "navigate", undefined, `Opening ${nav}`);
|
|
190
|
+
return unsupportedPlainCliIntent(argv);
|
|
191
|
+
}
|
|
192
|
+
function intentFromTypeBar(value) {
|
|
193
|
+
const raw = value.trim();
|
|
194
|
+
if (!raw)
|
|
195
|
+
return intent("fleet", [], "refresh", undefined, "Opening runs", value);
|
|
196
|
+
const parts = raw.split(/\s+/).filter(Boolean);
|
|
197
|
+
const [first = "", second = "", ...rest] = parts;
|
|
198
|
+
const firstScene = NAV_SCENES.get(first.toLowerCase());
|
|
199
|
+
if (first === "tasks" || first === "task") {
|
|
200
|
+
if (second === "run") {
|
|
201
|
+
const task = explicitTaskIdArg(rest);
|
|
202
|
+
if (rest.includes("--next") || task === "next")
|
|
203
|
+
return intent("tasks", parts, "task-run-next", undefined, "Dispatching next task", value);
|
|
204
|
+
if (task)
|
|
205
|
+
return intent("tasks", parts, "task-run-id", { task }, `Dispatching ${task}`, value);
|
|
206
|
+
return intent("tasks", parts, "refresh", undefined, "Select a task, then press Enter; or use run next", value);
|
|
207
|
+
}
|
|
208
|
+
if (!isTaskViewCommand(second.toLowerCase()))
|
|
209
|
+
return unsupportedPlainCliIntent(parts, value);
|
|
210
|
+
return taskViewIntent(parts, second, rest, value);
|
|
211
|
+
}
|
|
212
|
+
if (first === "run") {
|
|
213
|
+
if (second === "next" || second === "--next")
|
|
214
|
+
return intent("tasks", parts, "task-run-next", undefined, "Dispatching next task", value);
|
|
215
|
+
if (second === "--task") {
|
|
216
|
+
const task = explicitTaskIdArg([second, ...rest]);
|
|
217
|
+
if (task)
|
|
218
|
+
return intent("tasks", parts, "task-run-id", { task }, `Dispatching ${task}`, value);
|
|
219
|
+
return intent("tasks", parts, "refresh", undefined, "Select a task, then press Enter; or use run next", value);
|
|
220
|
+
}
|
|
221
|
+
if (second && !second.startsWith("-"))
|
|
222
|
+
return intent("tasks", parts, "task-run-id", { task: second }, `Dispatching ${second}`, value);
|
|
223
|
+
return intent("tasks", parts, "refresh", undefined, "Select a task, then press Enter; or use run next", value);
|
|
224
|
+
}
|
|
225
|
+
if (firstScene && parts.length === 1) {
|
|
226
|
+
return intent(firstScene, parts, "navigate", undefined, `Opening ${firstScene}`, value);
|
|
227
|
+
}
|
|
228
|
+
if (first === "stop")
|
|
229
|
+
return intent("fleet", parts, "run-stop", { runId: second || undefined }, second ? `Stopping ${second}` : "Stopping run", value);
|
|
230
|
+
if (first === "attach") {
|
|
231
|
+
if (!second)
|
|
232
|
+
return intent("handoff", parts, "navigate", undefined, "Select a run to attach", value);
|
|
233
|
+
return intent("handoff", parts, "run-attach", { runId: second }, `Attaching ${second}`, value);
|
|
234
|
+
}
|
|
235
|
+
return intentFromArgv(parts);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// packages/cli/src/app-opentui/layout.ts
|
|
239
|
+
function computeStageLayout(width, height) {
|
|
240
|
+
const safeWidth = Math.max(40, Math.trunc(width || 80));
|
|
241
|
+
const safeHeight = Math.max(18, Math.trunc(height || 24));
|
|
242
|
+
const compact = safeWidth < 92 || safeHeight < 30;
|
|
243
|
+
const horizontalGutter = compact ? 1 : 2;
|
|
244
|
+
const centerWidth = Math.max(36, safeWidth - horizontalGutter * 2);
|
|
245
|
+
const centerLeft = horizontalGutter;
|
|
246
|
+
const typeBarTop = Math.max(0, safeHeight - 3);
|
|
247
|
+
const footerTop = Math.max(0, safeHeight - 1);
|
|
248
|
+
const centerTop = compact ? 1 : Math.max(1, Math.floor(safeHeight * 0.055));
|
|
249
|
+
const centerHeight = Math.max(8, typeBarTop - centerTop - 1);
|
|
250
|
+
return { width: safeWidth, height: safeHeight, centerWidth, centerLeft, centerTop, centerHeight, typeBarTop, footerTop, compact };
|
|
251
|
+
}
|
|
252
|
+
function visibleWidth(text) {
|
|
253
|
+
return [...text].length;
|
|
254
|
+
}
|
|
255
|
+
function alignText(text, width, align = "left") {
|
|
256
|
+
const length = visibleWidth(text);
|
|
257
|
+
if (length >= width)
|
|
258
|
+
return text;
|
|
259
|
+
const pad = width - length;
|
|
260
|
+
if (align === "right")
|
|
261
|
+
return `${" ".repeat(pad)}${text}`;
|
|
262
|
+
if (align === "center") {
|
|
263
|
+
const left = Math.floor(pad / 2);
|
|
264
|
+
return `${" ".repeat(left)}${text}${" ".repeat(pad - left)}`;
|
|
265
|
+
}
|
|
266
|
+
return text;
|
|
267
|
+
}
|
|
268
|
+
function truncateText(text, width) {
|
|
269
|
+
if (visibleWidth(text) <= width)
|
|
270
|
+
return text;
|
|
271
|
+
if (width <= 1)
|
|
272
|
+
return "\u2026";
|
|
273
|
+
return `${[...text].slice(0, Math.max(0, width - 1)).join("")}\u2026`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// packages/cli/src/app-opentui/list-search.ts
|
|
277
|
+
function isListSearchScene(scene) {
|
|
278
|
+
return scene === "fleet" || scene === "tasks";
|
|
279
|
+
}
|
|
280
|
+
function listSearchQuery(state) {
|
|
281
|
+
return isListSearchScene(state.scene) ? state.typeBar.value.trim() : "";
|
|
282
|
+
}
|
|
283
|
+
function valueText(value) {
|
|
284
|
+
if (value === undefined || value === null)
|
|
285
|
+
return "";
|
|
286
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
287
|
+
return String(value);
|
|
288
|
+
if (Array.isArray(value))
|
|
289
|
+
return value.map(valueText).join(" ");
|
|
290
|
+
if (typeof value === "object") {
|
|
291
|
+
const record = value;
|
|
292
|
+
return ["id", "runId", "taskId", "number", "title", "name", "summary", "status", "state", "label", "labels", "assignee", "assignees", "url"].map((key) => valueText(record[key])).join(" ");
|
|
293
|
+
}
|
|
294
|
+
return "";
|
|
295
|
+
}
|
|
296
|
+
function includesQuery(haystack, query) {
|
|
297
|
+
const needle = query.trim().toLowerCase();
|
|
298
|
+
if (!needle)
|
|
299
|
+
return true;
|
|
300
|
+
return haystack.map(valueText).join(" ").toLowerCase().includes(needle);
|
|
301
|
+
}
|
|
302
|
+
function filterRunsForSearch(runs, query) {
|
|
303
|
+
return query.trim() ? runs.filter((run) => includesQuery([run.runId, run.status, run.title, run.taskId, run.raw], query)) : [...runs];
|
|
304
|
+
}
|
|
305
|
+
function filterTasksForSearch(tasks, query) {
|
|
306
|
+
return query.trim() ? tasks.filter((task) => includesQuery([task.id, task.status, task.title, task.priority, task.labels, task.assignees, task.raw], query)) : [...tasks];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// packages/cli/src/app-opentui/state.ts
|
|
310
|
+
function mergePatch(state, patch) {
|
|
311
|
+
return {
|
|
312
|
+
...state,
|
|
313
|
+
...patch,
|
|
314
|
+
typeBar: patch.typeBar ? { ...state.typeBar, ...patch.typeBar } : state.typeBar,
|
|
315
|
+
footer: patch.footer ? { ...state.footer, ...patch.footer } : state.footer,
|
|
316
|
+
selection: patch.selection ? { ...state.selection, ...patch.selection } : state.selection,
|
|
317
|
+
data: patch.data ? { ...state.data, ...patch.data } : state.data
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function createInitialAppState(options) {
|
|
321
|
+
const initialIntent = intentFromArgv(options.argv);
|
|
322
|
+
const projectName = options.projectRoot.split(/[\\/]/).filter(Boolean).at(-1) ?? options.projectRoot;
|
|
323
|
+
return {
|
|
324
|
+
projectRoot: options.projectRoot,
|
|
325
|
+
scene: initialIntent.scene,
|
|
326
|
+
status: initialIntent.action.kind === "none" || initialIntent.action.kind === "navigate" ? "idle" : "loading",
|
|
327
|
+
tick: 0,
|
|
328
|
+
argv: options.argv,
|
|
329
|
+
intent: initialIntent,
|
|
330
|
+
typeBar: {
|
|
331
|
+
value: "",
|
|
332
|
+
placeholder: "runs \xB7 tasks \xB7 help \xB7 attach <run>"
|
|
333
|
+
},
|
|
334
|
+
footer: {
|
|
335
|
+
project: projectName,
|
|
336
|
+
...options.initialFooter ?? {}
|
|
337
|
+
},
|
|
338
|
+
selection: { index: 0, count: 0 },
|
|
339
|
+
data: {
|
|
340
|
+
projectConfigured: options.projectConfigured ?? false
|
|
341
|
+
},
|
|
342
|
+
runtimeReady: false,
|
|
343
|
+
actionLabel: initialIntent.action.label
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function createAppStore(initialState) {
|
|
347
|
+
let state = initialState;
|
|
348
|
+
const listeners = new Set;
|
|
349
|
+
const notify = () => {
|
|
350
|
+
for (const listener of [...listeners])
|
|
351
|
+
listener(state);
|
|
352
|
+
};
|
|
353
|
+
return {
|
|
354
|
+
getState() {
|
|
355
|
+
return state;
|
|
356
|
+
},
|
|
357
|
+
setState(next) {
|
|
358
|
+
state = next;
|
|
359
|
+
notify();
|
|
360
|
+
},
|
|
361
|
+
patch(patch) {
|
|
362
|
+
state = mergePatch(state, patch);
|
|
363
|
+
notify();
|
|
364
|
+
},
|
|
365
|
+
subscribe(listener) {
|
|
366
|
+
listeners.add(listener);
|
|
367
|
+
return () => listeners.delete(listener);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function reduceAppEvent(state, event) {
|
|
372
|
+
switch (event.type) {
|
|
373
|
+
case "scene.change":
|
|
374
|
+
return {
|
|
375
|
+
scene: event.scene,
|
|
376
|
+
...event.intent ? { intent: event.intent, actionLabel: event.intent.action.label } : {},
|
|
377
|
+
status: "idle",
|
|
378
|
+
error: undefined,
|
|
379
|
+
typeBar: { message: undefined }
|
|
380
|
+
};
|
|
381
|
+
case "action.started":
|
|
382
|
+
return {
|
|
383
|
+
status: "action",
|
|
384
|
+
actionLabel: event.label,
|
|
385
|
+
error: undefined,
|
|
386
|
+
data: event.optimistic ?? {},
|
|
387
|
+
typeBar: { message: event.label }
|
|
388
|
+
};
|
|
389
|
+
case "action.progress":
|
|
390
|
+
return {
|
|
391
|
+
status: "action",
|
|
392
|
+
actionLabel: event.label,
|
|
393
|
+
data: event.data ?? {},
|
|
394
|
+
typeBar: { message: event.detail ?? event.label }
|
|
395
|
+
};
|
|
396
|
+
case "action.completed":
|
|
397
|
+
return {
|
|
398
|
+
status: "success",
|
|
399
|
+
actionLabel: event.label,
|
|
400
|
+
data: event.data ?? {},
|
|
401
|
+
typeBar: { message: `${event.label} complete` }
|
|
402
|
+
};
|
|
403
|
+
case "action.failed":
|
|
404
|
+
return {
|
|
405
|
+
status: "error",
|
|
406
|
+
actionLabel: event.label,
|
|
407
|
+
error: { message: event.message, hint: event.hint, cause: event.cause },
|
|
408
|
+
typeBar: { message: event.message, placeholder: event.hint ?? "runs \xB7 tasks \xB7 help" }
|
|
409
|
+
};
|
|
410
|
+
case "data.patch":
|
|
411
|
+
return { data: event.data };
|
|
412
|
+
case "footer.patch":
|
|
413
|
+
return { footer: event.footer };
|
|
414
|
+
case "typebar.patch":
|
|
415
|
+
return { typeBar: event.typeBar };
|
|
416
|
+
case "runtime.ready":
|
|
417
|
+
return { runtimeReady: true, footer: { message: "runtime ready" } };
|
|
418
|
+
case "runtime.failed":
|
|
419
|
+
return {
|
|
420
|
+
scene: "error",
|
|
421
|
+
status: "error",
|
|
422
|
+
runtimeReady: false,
|
|
423
|
+
error: { message: event.message, hint: event.hint, cause: event.cause }
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// packages/cli/src/app-opentui/theme.ts
|
|
429
|
+
import {
|
|
430
|
+
bold as otuiBold,
|
|
431
|
+
dim as otuiDim,
|
|
432
|
+
fg as otuiFg,
|
|
433
|
+
t,
|
|
434
|
+
TextAttributes
|
|
435
|
+
} from "@opentui/core";
|
|
436
|
+
var RIG_UI = {
|
|
437
|
+
bg: "#070809",
|
|
438
|
+
bg2: "#0b0c0e",
|
|
439
|
+
panel: "#101115",
|
|
440
|
+
panel2: "#101115",
|
|
441
|
+
glass: "#14161b",
|
|
442
|
+
ink: "#f2f3f6",
|
|
443
|
+
ink2: "#aeb0ba",
|
|
444
|
+
ink3: "#6c6e79",
|
|
445
|
+
ink4: "#44464f",
|
|
446
|
+
lime: "#ccff4d",
|
|
447
|
+
limeDim: "#a9d63f",
|
|
448
|
+
cyan: "#56d8ff",
|
|
449
|
+
red: "#ff5d5d",
|
|
450
|
+
yellow: "#ffd24d",
|
|
451
|
+
magenta: "#ff79b0"
|
|
452
|
+
};
|
|
453
|
+
var styles = {
|
|
454
|
+
ink: otuiFg(RIG_UI.ink),
|
|
455
|
+
ink2: otuiFg(RIG_UI.ink2),
|
|
456
|
+
ink3: otuiFg(RIG_UI.ink3),
|
|
457
|
+
ink4: otuiFg(RIG_UI.ink4),
|
|
458
|
+
lime: otuiFg(RIG_UI.lime),
|
|
459
|
+
limeDim: otuiFg(RIG_UI.limeDim),
|
|
460
|
+
cyan: otuiFg(RIG_UI.cyan),
|
|
461
|
+
red: otuiFg(RIG_UI.red),
|
|
462
|
+
yellow: otuiFg(RIG_UI.yellow),
|
|
463
|
+
magenta: otuiFg(RIG_UI.magenta)
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// packages/cli/src/app-opentui/render/graphics.ts
|
|
467
|
+
import { FrameBufferRenderable, RGBA } from "@opentui/core";
|
|
468
|
+
var TRANSPARENT = RGBA.fromValues(0, 0, 0, 0);
|
|
469
|
+
var BACKDROP = RGBA.fromHex(RIG_UI.bg);
|
|
470
|
+
var PANEL_BG = RGBA.fromInts(16, 17, 21, 184);
|
|
471
|
+
var PANEL_HEADER_BG = RGBA.fromInts(16, 17, 21, 188);
|
|
472
|
+
var PANEL_LINE = RGBA.fromInts(255, 255, 255, 20);
|
|
473
|
+
var PANEL_LINE_DIM = RGBA.fromInts(255, 255, 255, 8);
|
|
474
|
+
var PANEL_TEXT_DIM = RGBA.fromInts(108, 110, 121, 255);
|
|
475
|
+
var AC_RGB = [204, 255, 77];
|
|
476
|
+
var C2_RGB = [86, 216, 255];
|
|
477
|
+
var MG_RGB = [255, 121, 176];
|
|
478
|
+
var INK_RGB = [108, 110, 121];
|
|
479
|
+
var BRAILLE_BITS = [
|
|
480
|
+
[1, 8],
|
|
481
|
+
[2, 16],
|
|
482
|
+
[4, 32],
|
|
483
|
+
[64, 128]
|
|
484
|
+
];
|
|
485
|
+
function sceneKind(scene) {
|
|
486
|
+
if (scene === "tasks")
|
|
487
|
+
return "loop";
|
|
488
|
+
if (scene === "help")
|
|
489
|
+
return "carrier";
|
|
490
|
+
return "dispatch";
|
|
491
|
+
}
|
|
492
|
+
function seeded(seed) {
|
|
493
|
+
let value = seed >>> 0;
|
|
494
|
+
return () => {
|
|
495
|
+
value = value * 1664525 + 1013904223 >>> 0;
|
|
496
|
+
return value / 4294967295;
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function channelBuffer(canvas, channel) {
|
|
500
|
+
switch (channel) {
|
|
501
|
+
case 0:
|
|
502
|
+
return canvas.ac;
|
|
503
|
+
case 1:
|
|
504
|
+
return canvas.c2;
|
|
505
|
+
case 2:
|
|
506
|
+
return canvas.mg;
|
|
507
|
+
case 3:
|
|
508
|
+
return canvas.ink;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function addSample(canvas, channel, x, y, value) {
|
|
512
|
+
const ix = Math.trunc(x);
|
|
513
|
+
const iy = Math.trunc(y);
|
|
514
|
+
if (ix < 0 || iy < 0 || ix >= canvas.width || iy >= canvas.height)
|
|
515
|
+
return;
|
|
516
|
+
const buffer = channelBuffer(canvas, channel);
|
|
517
|
+
const index = iy * canvas.width + ix;
|
|
518
|
+
buffer[index] = Math.min(1, buffer[index] + value);
|
|
519
|
+
}
|
|
520
|
+
function drawDot(canvas, channel, cx, cy, radius, value) {
|
|
521
|
+
const minX = Math.floor(cx - radius);
|
|
522
|
+
const maxX = Math.ceil(cx + radius);
|
|
523
|
+
const minY = Math.floor(cy - radius);
|
|
524
|
+
const maxY = Math.ceil(cy + radius);
|
|
525
|
+
for (let y = minY;y <= maxY; y += 1) {
|
|
526
|
+
for (let x = minX;x <= maxX; x += 1) {
|
|
527
|
+
const dx = x - cx;
|
|
528
|
+
const dy = y - cy;
|
|
529
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
530
|
+
if (distance > radius)
|
|
531
|
+
continue;
|
|
532
|
+
const falloff = 1 - distance / Math.max(0.001, radius);
|
|
533
|
+
addSample(canvas, channel, x, y, value * (0.28 + falloff * 0.72));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function drawGlow(canvas, channel, cx, cy, radius, value) {
|
|
538
|
+
const minX = Math.floor(cx - radius);
|
|
539
|
+
const maxX = Math.ceil(cx + radius);
|
|
540
|
+
const minY = Math.floor(cy - radius);
|
|
541
|
+
const maxY = Math.ceil(cy + radius);
|
|
542
|
+
for (let y = minY;y <= maxY; y += 1) {
|
|
543
|
+
for (let x = minX;x <= maxX; x += 1) {
|
|
544
|
+
const dx = x - cx;
|
|
545
|
+
const dy = y - cy;
|
|
546
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
547
|
+
if (distance > radius)
|
|
548
|
+
continue;
|
|
549
|
+
const falloff = Math.pow(1 - distance / Math.max(0.001, radius), 2.2);
|
|
550
|
+
addSample(canvas, channel, x, y, value * falloff);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function drawLine(canvas, channel, x0, y0, x1, y1, value, width = 0.8) {
|
|
555
|
+
const dx = x1 - x0;
|
|
556
|
+
const dy = y1 - y0;
|
|
557
|
+
const steps = Math.max(1, Math.ceil(Math.hypot(dx, dy) * 1.7));
|
|
558
|
+
for (let step = 0;step <= steps; step += 1) {
|
|
559
|
+
const t2 = step / steps;
|
|
560
|
+
const x = x0 + dx * t2;
|
|
561
|
+
const y = y0 + dy * t2;
|
|
562
|
+
drawDot(canvas, channel, x, y, width, value);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function drawCircleStroke(canvas, channel, cx, cy, radius, value) {
|
|
566
|
+
const steps = Math.max(18, Math.ceil(radius * 8));
|
|
567
|
+
for (let step = 0;step < steps; step += 1) {
|
|
568
|
+
const a = step / steps * Math.PI * 2;
|
|
569
|
+
drawDot(canvas, channel, cx + Math.cos(a) * radius, cy + Math.sin(a) * radius, 0.72, value);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function paletteColor(ac, c2, mg, ink) {
|
|
573
|
+
const total = Math.max(0.001, ac + c2 + mg + ink);
|
|
574
|
+
const r = (AC_RGB[0] * ac + C2_RGB[0] * c2 + MG_RGB[0] * mg + INK_RGB[0] * ink) / total;
|
|
575
|
+
const g = (AC_RGB[1] * ac + C2_RGB[1] * c2 + MG_RGB[1] * mg + INK_RGB[1] * ink) / total;
|
|
576
|
+
const b = (AC_RGB[2] * ac + C2_RGB[2] * c2 + MG_RGB[2] * mg + INK_RGB[2] * ink) / total;
|
|
577
|
+
const intensity = Math.min(1, total / 4);
|
|
578
|
+
const lift = 0.34 + intensity * 0.66;
|
|
579
|
+
return RGBA.fromInts(Math.round(r * lift), Math.round(g * lift), Math.round(b * lift), 255);
|
|
580
|
+
}
|
|
581
|
+
function clearCanvas(canvas) {
|
|
582
|
+
canvas.ac.fill(0);
|
|
583
|
+
canvas.c2.fill(0);
|
|
584
|
+
canvas.mg.fill(0);
|
|
585
|
+
canvas.ink.fill(0);
|
|
586
|
+
}
|
|
587
|
+
function makeCanvas(layout) {
|
|
588
|
+
const width = Math.max(1, layout.width * 2);
|
|
589
|
+
const height = Math.max(1, layout.height * 4);
|
|
590
|
+
const size = width * height;
|
|
591
|
+
return {
|
|
592
|
+
width,
|
|
593
|
+
height,
|
|
594
|
+
ac: new Float32Array(size),
|
|
595
|
+
c2: new Float32Array(size),
|
|
596
|
+
mg: new Float32Array(size),
|
|
597
|
+
ink: new Float32Array(size)
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
class MarketingGlyphCanvas {
|
|
602
|
+
rand = seeded(6221137);
|
|
603
|
+
canvas;
|
|
604
|
+
layout;
|
|
605
|
+
activeKind = null;
|
|
606
|
+
lanes = [];
|
|
607
|
+
motes = [];
|
|
608
|
+
packets = [];
|
|
609
|
+
constructor(layout) {
|
|
610
|
+
this.layout = layout;
|
|
611
|
+
this.canvas = makeCanvas(layout);
|
|
612
|
+
this.reset("dispatch");
|
|
613
|
+
}
|
|
614
|
+
resize(layout) {
|
|
615
|
+
if (layout.width === this.layout.width && layout.height === this.layout.height)
|
|
616
|
+
return;
|
|
617
|
+
this.layout = layout;
|
|
618
|
+
this.canvas = makeCanvas(layout);
|
|
619
|
+
this.reset(this.activeKind ?? "dispatch");
|
|
620
|
+
}
|
|
621
|
+
render(layer, scene, tick) {
|
|
622
|
+
const kind = sceneKind(scene);
|
|
623
|
+
if (kind !== this.activeKind)
|
|
624
|
+
this.reset(kind);
|
|
625
|
+
clearCanvas(this.canvas);
|
|
626
|
+
this.drawMarketingRails(kind, tick);
|
|
627
|
+
if (kind === "loop")
|
|
628
|
+
this.drawLoop(tick);
|
|
629
|
+
else if (kind === "carrier")
|
|
630
|
+
this.drawCarrier(tick);
|
|
631
|
+
else
|
|
632
|
+
this.drawDispatch(tick);
|
|
633
|
+
this.blit(layer);
|
|
634
|
+
}
|
|
635
|
+
reset(kind) {
|
|
636
|
+
this.activeKind = kind;
|
|
637
|
+
this.lanes = [];
|
|
638
|
+
this.motes = [];
|
|
639
|
+
this.packets = [];
|
|
640
|
+
if (kind === "dispatch")
|
|
641
|
+
this.setupDispatch();
|
|
642
|
+
else if (kind === "loop")
|
|
643
|
+
this.setupLoop();
|
|
644
|
+
else
|
|
645
|
+
this.setupCarrier();
|
|
646
|
+
}
|
|
647
|
+
setupDispatch() {
|
|
648
|
+
const count = this.layout.compact ? 7 : 12;
|
|
649
|
+
for (let i = 0;i < count; i += 1) {
|
|
650
|
+
const ang = (-0.9 + 1.8 * (i / Math.max(1, count - 1))) * (Math.PI * 0.5);
|
|
651
|
+
this.lanes.push({ ang, len: this.canvas.width * (0.32 + this.rand() * 0.42), curve: (this.rand() - 0.5) * 0.7, phase: this.rand() * Math.PI * 2 });
|
|
652
|
+
}
|
|
653
|
+
const packets = this.layout.compact ? 70 : 150;
|
|
654
|
+
for (let i = 0;i < packets; i += 1)
|
|
655
|
+
this.packets.push(this.packet(true));
|
|
656
|
+
}
|
|
657
|
+
setupLoop() {
|
|
658
|
+
const lanes = this.layout.compact ? 4 : 7;
|
|
659
|
+
for (let i = 0;i < lanes; i += 1)
|
|
660
|
+
this.packets.push(this.packet(true));
|
|
661
|
+
}
|
|
662
|
+
setupCarrier() {
|
|
663
|
+
const nodes = this.layout.compact ? 5 : 7;
|
|
664
|
+
for (let i = 0;i < nodes * 6; i += 1)
|
|
665
|
+
this.packets.push(this.packet(true));
|
|
666
|
+
}
|
|
667
|
+
packet(random = false) {
|
|
668
|
+
return {
|
|
669
|
+
lane: Math.floor(this.rand() * Math.max(1, this.lanes.length || 7)),
|
|
670
|
+
p: random ? this.rand() : 0,
|
|
671
|
+
spd: 0.003 + this.rand() * 0.009,
|
|
672
|
+
channel: this.rand() < 0.22 ? 1 : 0,
|
|
673
|
+
wobble: (this.rand() - 0.5) * 7,
|
|
674
|
+
size: 0.8 + this.rand() * 1.3
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
core(kind) {
|
|
678
|
+
if (kind === "dispatch")
|
|
679
|
+
return { x: this.canvas.width * 0.3, y: this.canvas.height * 0.45 };
|
|
680
|
+
if (kind === "carrier")
|
|
681
|
+
return { x: this.canvas.width * 0.5, y: this.canvas.height * 0.54 };
|
|
682
|
+
return { x: this.canvas.width * 0.09, y: this.canvas.height * 0.56 };
|
|
683
|
+
}
|
|
684
|
+
lanePoint(lane, p, kind = "dispatch") {
|
|
685
|
+
const core = this.core(kind);
|
|
686
|
+
const dist = lane.len * p;
|
|
687
|
+
const bend = Math.sin(p * Math.PI) * lane.curve * 28;
|
|
688
|
+
const ux = Math.cos(lane.ang);
|
|
689
|
+
const uy = Math.sin(lane.ang);
|
|
690
|
+
return { x: core.x + ux * dist + -uy * bend, y: core.y + uy * dist + ux * bend };
|
|
691
|
+
}
|
|
692
|
+
drawMotes(tick) {
|
|
693
|
+
for (const mote of this.motes) {
|
|
694
|
+
mote.x += 0.1 * mote.z;
|
|
695
|
+
if (mote.x > this.canvas.width + 2)
|
|
696
|
+
mote.x = -2;
|
|
697
|
+
const alpha = (0.035 + 0.08 * mote.z) * (0.55 + 0.45 * Math.sin(tick * 0.03 + mote.tw));
|
|
698
|
+
drawDot(this.canvas, mote.channel, mote.x, mote.y, 0.45 * mote.z, alpha);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
drawMarketingRails(kind, tick) {
|
|
702
|
+
const ys = kind === "loop" ? [0.18, 0.5, 0.82] : kind === "carrier" ? [0.24, 0.54, 0.82] : [0.18, 0.58, 0.82];
|
|
703
|
+
ys.forEach((ratio, index) => {
|
|
704
|
+
const y = this.canvas.height * ratio + Math.sin(tick * 0.018 + index) * 1.8;
|
|
705
|
+
drawLine(this.canvas, index === 1 ? 0 : 1, this.canvas.width * 0.06, y, this.canvas.width * 0.94, y + Math.sin(index) * 2, index === 1 ? 0.11 : 0.07, 0.55);
|
|
706
|
+
for (let p = (tick * 0.004 + index * 0.21) % 1;p < 1; p += 0.18 + index * 0.02) {
|
|
707
|
+
const x = this.canvas.width * (0.06 + 0.88 * p);
|
|
708
|
+
drawDot(this.canvas, index === 2 ? 1 : 0, x, y, 1 + index * 0.25, 0.24);
|
|
709
|
+
drawGlow(this.canvas, index === 2 ? 1 : 0, x, y, 3.8, 0.035);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
drawDispatch(tick) {
|
|
714
|
+
const core = this.core("dispatch");
|
|
715
|
+
drawGlow(this.canvas, 0, core.x, core.y, 30, 0.12);
|
|
716
|
+
drawDot(this.canvas, 0, core.x, core.y, 2.2, 0.85);
|
|
717
|
+
for (const lane of this.lanes) {
|
|
718
|
+
let prev = this.lanePoint(lane, 0);
|
|
719
|
+
for (let i = 1;i <= 32; i += 1) {
|
|
720
|
+
const next = this.lanePoint(lane, i / 32);
|
|
721
|
+
drawLine(this.canvas, lane.phase > Math.PI ? 1 : 0, prev.x, prev.y, next.x, next.y, 0.045, 0.38);
|
|
722
|
+
prev = next;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
for (const packet of this.packets) {
|
|
726
|
+
packet.p += packet.spd;
|
|
727
|
+
if (packet.p > 1)
|
|
728
|
+
Object.assign(packet, this.packet(false));
|
|
729
|
+
const lane = this.lanes[packet.lane % Math.max(1, this.lanes.length)];
|
|
730
|
+
if (!lane)
|
|
731
|
+
continue;
|
|
732
|
+
const pt = this.lanePoint(lane, packet.p);
|
|
733
|
+
const fade = packet.p < 0.1 ? packet.p / 0.1 : packet.p > 0.88 ? (1 - packet.p) / 0.12 : 1;
|
|
734
|
+
const y = pt.y + packet.wobble * Math.sin(packet.p * Math.PI * 2);
|
|
735
|
+
drawLine(this.canvas, packet.channel, pt.x - 4, y - 1, pt.x, y, 0.08 * fade, 0.38);
|
|
736
|
+
drawDot(this.canvas, packet.channel, pt.x, y, packet.size, 0.5 * fade);
|
|
737
|
+
drawGlow(this.canvas, packet.channel, pt.x, y, packet.size * 4, 0.045 * fade);
|
|
738
|
+
}
|
|
739
|
+
for (let i = 0;i < (this.layout.compact ? 4 : 8); i += 1) {
|
|
740
|
+
const lane = this.lanes[i % Math.max(1, this.lanes.length)];
|
|
741
|
+
if (!lane)
|
|
742
|
+
continue;
|
|
743
|
+
const p = (tick * 0.002 + i * 0.13) % 1;
|
|
744
|
+
const pt = this.lanePoint(lane, p);
|
|
745
|
+
this.drawDrone(pt.x, pt.y + Math.sin(tick * 0.04 + i) * 2, 2.6 + i % 2 * 0.6, tick + i * 11, i % 3 === 0 ? 1 : 0);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
drawLoop(tick) {
|
|
749
|
+
const y = this.canvas.height * 0.56;
|
|
750
|
+
const x0 = this.canvas.width * 0.09;
|
|
751
|
+
const x1 = this.canvas.width * 0.91;
|
|
752
|
+
drawLine(this.canvas, 0, x0, y, x1, y, 0.12, 0.55);
|
|
753
|
+
const gates = this.layout.compact ? 4 : 5;
|
|
754
|
+
const active = Math.floor(tick / 16) % gates;
|
|
755
|
+
for (let i = 0;i < gates; i += 1) {
|
|
756
|
+
const x = x0 + (x1 - x0) * (i / Math.max(1, gates - 1));
|
|
757
|
+
drawLine(this.canvas, i === active ? 0 : 3, x, y - 22, x, y + 22, i === active ? 0.25 : 0.08, i === active ? 0.7 : 0.42);
|
|
758
|
+
drawDot(this.canvas, i <= active ? 0 : 3, x, y, i === active ? 2.2 : 1.2, i === active ? 0.72 : 0.25);
|
|
759
|
+
drawCircleStroke(this.canvas, i <= active ? 0 : 3, x, y, i === active ? 4.2 : 2.4, i === active ? 0.18 : 0.08);
|
|
760
|
+
}
|
|
761
|
+
for (const packet of this.packets) {
|
|
762
|
+
packet.p += packet.spd * 0.7;
|
|
763
|
+
if (packet.p > 1)
|
|
764
|
+
packet.p = 0;
|
|
765
|
+
const x = x0 + (x1 - x0) * packet.p;
|
|
766
|
+
const segment = Math.min(gates - 1, Math.floor(packet.p * gates));
|
|
767
|
+
const channel = segment < active ? 0 : segment === active ? 1 : 3;
|
|
768
|
+
const py = y + packet.wobble * Math.sin(packet.p * Math.PI * 2);
|
|
769
|
+
drawLine(this.canvas, channel, x - 6, py, x, py, 0.1, 0.42);
|
|
770
|
+
drawDot(this.canvas, channel, x, py, packet.size, channel === 3 ? 0.22 : 0.58);
|
|
771
|
+
drawGlow(this.canvas, channel, x, py, 3.8, channel === 3 ? 0.02 : 0.04);
|
|
772
|
+
}
|
|
773
|
+
this.drawDrone(x0 + (x1 - x0) * (tick * 0.003 % 1), y - 18, 2.7, tick, 0);
|
|
774
|
+
this.drawDrone(x0 + (x1 - x0) * ((tick * 0.002 + 0.5) % 1), y + 16, 2.2, tick + 40, 2);
|
|
775
|
+
}
|
|
776
|
+
drawCarrier(tick) {
|
|
777
|
+
const core = this.core("carrier");
|
|
778
|
+
drawGlow(this.canvas, 0, core.x, core.y, 26, 0.12);
|
|
779
|
+
drawDot(this.canvas, 0, core.x, core.y, 2.4, 0.92);
|
|
780
|
+
drawCircleStroke(this.canvas, 0, core.x, core.y, 8 + Math.sin(tick * 0.06) * 2, 0.14);
|
|
781
|
+
const nodes = this.layout.compact ? 5 : 7;
|
|
782
|
+
const rx = this.canvas.width * 0.36;
|
|
783
|
+
const ry = this.canvas.height * 0.22;
|
|
784
|
+
for (let i = 0;i < nodes; i += 1) {
|
|
785
|
+
const a = i / nodes * Math.PI * 2 - Math.PI / 2 + tick * 0.006;
|
|
786
|
+
const x = core.x + Math.cos(a) * rx;
|
|
787
|
+
const y = core.y + Math.sin(a) * ry;
|
|
788
|
+
drawLine(this.canvas, i % 2 ? 1 : 0, core.x, core.y, x, y, 0.055, 0.36);
|
|
789
|
+
drawCircleStroke(this.canvas, i % 2 ? 1 : 0, x, y, 4.2, 0.15);
|
|
790
|
+
drawDot(this.canvas, i % 2 ? 1 : 0, x, y, 1.7, 0.62);
|
|
791
|
+
this.drawDrone(x, y - 8, 2.1, tick + i * 9, i % 2 ? 1 : 0);
|
|
792
|
+
}
|
|
793
|
+
for (const packet of this.packets) {
|
|
794
|
+
packet.p += packet.spd;
|
|
795
|
+
if (packet.p > 1)
|
|
796
|
+
packet.p = 0;
|
|
797
|
+
const lane = packet.lane % nodes;
|
|
798
|
+
const a = lane / nodes * Math.PI * 2 - Math.PI / 2 + tick * 0.006;
|
|
799
|
+
const nx = core.x + Math.cos(a) * rx;
|
|
800
|
+
const ny = core.y + Math.sin(a) * ry;
|
|
801
|
+
const x = core.x + (nx - core.x) * packet.p;
|
|
802
|
+
const y = core.y + (ny - core.y) * packet.p;
|
|
803
|
+
drawDot(this.canvas, packet.channel, x, y, packet.size, 0.48);
|
|
804
|
+
drawGlow(this.canvas, packet.channel, x, y, 3.2, 0.035);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
drawDrone(x, y, scale, tick, channel) {
|
|
808
|
+
const wobble = Math.sin(tick * 0.08) * 0.7;
|
|
809
|
+
drawDot(this.canvas, channel, x, y + wobble, scale, 0.58);
|
|
810
|
+
drawDot(this.canvas, 3, x, y + wobble, Math.max(0.6, scale * 0.28), 0.42);
|
|
811
|
+
const rotor = scale * 1.75;
|
|
812
|
+
const arms = [
|
|
813
|
+
[-rotor, -rotor * 0.75],
|
|
814
|
+
[rotor, -rotor * 0.75],
|
|
815
|
+
[-rotor, rotor * 0.75],
|
|
816
|
+
[rotor, rotor * 0.75]
|
|
817
|
+
];
|
|
818
|
+
for (const [dx, dy] of arms) {
|
|
819
|
+
drawLine(this.canvas, channel, x, y + wobble, x + dx, y + dy + wobble, 0.11, 0.38);
|
|
820
|
+
drawCircleStroke(this.canvas, channel, x + dx, y + dy + wobble, scale * (0.65 + Math.abs(Math.sin(tick * 0.18)) * 0.25), 0.16);
|
|
821
|
+
drawDot(this.canvas, channel, x + dx, y + dy + wobble, 0.7, 0.25);
|
|
822
|
+
}
|
|
823
|
+
drawLine(this.canvas, channel === 1 ? 0 : 1, x - rotor * 2.4, y + wobble + 1.2, x - rotor * 1.2, y + wobble, 0.05, 0.35);
|
|
824
|
+
}
|
|
825
|
+
blit(layer) {
|
|
826
|
+
const fb = layer.frameBuffer;
|
|
827
|
+
fb.clear(BACKDROP);
|
|
828
|
+
const { width, height, ac, c2, mg, ink } = this.canvas;
|
|
829
|
+
for (let cy = 0;cy < this.layout.height; cy += 1) {
|
|
830
|
+
for (let cx = 0;cx < this.layout.width; cx += 1) {
|
|
831
|
+
let bits = 0;
|
|
832
|
+
let acSum = 0;
|
|
833
|
+
let c2Sum = 0;
|
|
834
|
+
let mgSum = 0;
|
|
835
|
+
let inkSum = 0;
|
|
836
|
+
for (let sy = 0;sy < 4; sy += 1) {
|
|
837
|
+
for (let sx = 0;sx < 2; sx += 1) {
|
|
838
|
+
const px = cx * 2 + sx;
|
|
839
|
+
const py = cy * 4 + sy;
|
|
840
|
+
if (px >= width || py >= height)
|
|
841
|
+
continue;
|
|
842
|
+
const index = py * width + px;
|
|
843
|
+
const a = ac[index];
|
|
844
|
+
const c = c2[index];
|
|
845
|
+
const m = mg[index];
|
|
846
|
+
const k = ink[index];
|
|
847
|
+
const total2 = a + c + m + k;
|
|
848
|
+
acSum += a;
|
|
849
|
+
c2Sum += c;
|
|
850
|
+
mgSum += m;
|
|
851
|
+
inkSum += k;
|
|
852
|
+
if (total2 > 0.11)
|
|
853
|
+
bits |= BRAILLE_BITS[sy][sx];
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const total = acSum + c2Sum + mgSum + inkSum;
|
|
857
|
+
if (bits === 0 || total <= 0.02)
|
|
858
|
+
continue;
|
|
859
|
+
const char = String.fromCharCode(10240 + bits);
|
|
860
|
+
fb.setCellWithAlphaBlending(cx, cy, char, paletteColor(acSum, c2Sum, mgSum, inkSum), TRANSPARENT);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function createGraphicsLayer(renderer, layout) {
|
|
866
|
+
const layer = new FrameBufferRenderable(renderer, {
|
|
867
|
+
id: "rig-marketing-glyph-canvas",
|
|
868
|
+
width: layout.width,
|
|
869
|
+
height: layout.height,
|
|
870
|
+
position: "absolute",
|
|
871
|
+
left: 0,
|
|
872
|
+
top: 0,
|
|
873
|
+
respectAlpha: true
|
|
874
|
+
});
|
|
875
|
+
layer.__rigMarketingCanvas = new MarketingGlyphCanvas(layout);
|
|
876
|
+
return layer;
|
|
877
|
+
}
|
|
878
|
+
function resizeGraphicsLayer(layer, layout) {
|
|
879
|
+
if (layer.width !== layout.width)
|
|
880
|
+
layer.width = layout.width;
|
|
881
|
+
if (layer.height !== layout.height)
|
|
882
|
+
layer.height = layout.height;
|
|
883
|
+
layer.__rigMarketingCanvas?.resize(layout);
|
|
884
|
+
}
|
|
885
|
+
function clearGraphicsLayer(layer) {
|
|
886
|
+
layer.frameBuffer.clear(BACKDROP);
|
|
887
|
+
}
|
|
888
|
+
function panelRect(layout, panel) {
|
|
889
|
+
const left = Math.max(0, Math.min(layout.width - 1, layout.centerLeft + (panel.left ?? 0)));
|
|
890
|
+
const desiredWidth = panel.width ?? layout.centerWidth;
|
|
891
|
+
const width = Math.max(1, Math.min(layout.width - left, desiredWidth));
|
|
892
|
+
const top = Math.max(0, Math.min(layout.height - 1, layout.centerTop + panel.top));
|
|
893
|
+
const height = Math.max(1, Math.min(layout.height - top, panel.height));
|
|
894
|
+
return width > 3 && height > 3 ? { left, top, width, height } : null;
|
|
895
|
+
}
|
|
896
|
+
function writeCell(layer, x, y, char, fg, bg) {
|
|
897
|
+
layer.frameBuffer.setCellWithAlphaBlending(x, y, char, fg, bg);
|
|
898
|
+
}
|
|
899
|
+
function drawFullCell(layer, x, y, bg) {
|
|
900
|
+
writeCell(layer, x, y, " ", TRANSPARENT, bg);
|
|
901
|
+
}
|
|
902
|
+
function drawCornerCut(layer, x, y, char, fg) {
|
|
903
|
+
writeCell(layer, x, y, char, fg, BACKDROP);
|
|
904
|
+
}
|
|
905
|
+
function drawRoundedFill(layer, left, top, right, bottom, dividerY) {
|
|
906
|
+
const bgForY = (y) => y < dividerY ? PANEL_HEADER_BG : PANEL_BG;
|
|
907
|
+
for (let y = top;y <= bottom; y += 1) {
|
|
908
|
+
const bg = bgForY(y);
|
|
909
|
+
for (let x = left;x <= right; x += 1)
|
|
910
|
+
drawFullCell(layer, x, y, bg);
|
|
911
|
+
}
|
|
912
|
+
drawCornerCut(layer, left, top, "\u2597", PANEL_HEADER_BG);
|
|
913
|
+
drawCornerCut(layer, right, top, "\u2596", PANEL_HEADER_BG);
|
|
914
|
+
drawCornerCut(layer, left, bottom, "\u259D", PANEL_BG);
|
|
915
|
+
drawCornerCut(layer, right, bottom, "\u2598", PANEL_BG);
|
|
916
|
+
}
|
|
917
|
+
function drawAdTerminalPanel(layer, layout, panel) {
|
|
918
|
+
const rect = panelRect(layout, panel);
|
|
919
|
+
if (!rect)
|
|
920
|
+
return;
|
|
921
|
+
const { left, top, width, height } = rect;
|
|
922
|
+
const right = left + width - 1;
|
|
923
|
+
const bottom = top + height - 1;
|
|
924
|
+
const headerHeight = Math.max(3, Math.min(height - 2, panel.headerHeight ?? 3));
|
|
925
|
+
const dividerY = Math.min(bottom - 1, top + headerHeight);
|
|
926
|
+
drawRoundedFill(layer, left, top, right, bottom, dividerY);
|
|
927
|
+
if (dividerY > top && dividerY < bottom) {
|
|
928
|
+
for (let x = left + 2;x < right - 1; x += 1) {
|
|
929
|
+
writeCell(layer, x, dividerY, "\u2500", PANEL_LINE_DIM, PANEL_BG);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (panel.headerText) {
|
|
933
|
+
const x = left + Math.max(2, Math.min(5, panel.paddingX ?? 4));
|
|
934
|
+
const y = top + Math.max(1, Math.floor(headerHeight / 2));
|
|
935
|
+
layer.frameBuffer.drawText(panel.headerText.slice(0, Math.max(0, width - 8)), x, y, PANEL_TEXT_DIM, TRANSPARENT);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function drawPanelChrome(layer, layout, panels) {
|
|
939
|
+
for (const panel of panels) {
|
|
940
|
+
if (panel.chrome === "ad-terminal")
|
|
941
|
+
drawAdTerminalPanel(layer, layout, panel);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function drawAmbientField(layer, layout, tick, scene = "fleet") {
|
|
945
|
+
layer.__rigMarketingCanvas?.render(layer, scene, tick);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// packages/cli/src/app-opentui/render/image-visual-layer.ts
|
|
949
|
+
import { Worker } from "worker_threads";
|
|
950
|
+
import { deflateSync } from "zlib";
|
|
951
|
+
import { allocateImageId, deleteKittyImage, getCapabilities } from "@earendil-works/pi-tui";
|
|
952
|
+
var BG = [7, 8, 9, 255];
|
|
953
|
+
var PANEL = [16, 17, 21, 184];
|
|
954
|
+
var PANEL_HEADER = [16, 17, 21, 190];
|
|
955
|
+
var PANEL_LINE2 = [255, 255, 255, 24];
|
|
956
|
+
var LIME = [204, 255, 77, 255];
|
|
957
|
+
var LIME_DIM = [169, 214, 63, 255];
|
|
958
|
+
var CYAN = [86, 216, 255, 255];
|
|
959
|
+
var MAGENTA = [255, 121, 176, 255];
|
|
960
|
+
var INK_DIM = [108, 110, 121, 255];
|
|
961
|
+
var PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
962
|
+
var CRC_TABLE = makeCrcTable();
|
|
963
|
+
var MAX_IMAGE_BYTES = 48 * 1024 * 1024;
|
|
964
|
+
var KITTY_CHUNK_SIZE = 4096;
|
|
965
|
+
var PANEL_BLUR_SCALE = 5;
|
|
966
|
+
var PANEL_MASK_CACHE_LIMIT = 16;
|
|
967
|
+
var ZLIB_LEVEL = 1;
|
|
968
|
+
var DEFAULT_IMAGE_RESOLUTION_SCALE = 1;
|
|
969
|
+
var DEFAULT_DRONE_LAYER_SCALE = 0.18;
|
|
970
|
+
var DEFAULT_IMAGE_FPS = 24;
|
|
971
|
+
var panelMaskCache = new Map;
|
|
972
|
+
var BG_WORD = rgbaWord(BG);
|
|
973
|
+
function imageTransport() {
|
|
974
|
+
const forced = process.env.RIG_OPENTUI_IMAGE_TRANSPORT;
|
|
975
|
+
if (forced === "rgba-zlib")
|
|
976
|
+
return "rgba-zlib";
|
|
977
|
+
return "png";
|
|
978
|
+
}
|
|
979
|
+
function imageResolutionScale() {
|
|
980
|
+
const raw = Number.parseFloat(process.env.RIG_OPENTUI_IMAGE_SCALE ?? "");
|
|
981
|
+
if (Number.isFinite(raw) && raw > 0)
|
|
982
|
+
return Math.max(0.25, Math.min(1, raw));
|
|
983
|
+
return DEFAULT_IMAGE_RESOLUTION_SCALE;
|
|
984
|
+
}
|
|
985
|
+
function droneLayerScale() {
|
|
986
|
+
const raw = Number.parseFloat(process.env.RIG_OPENTUI_DRONE_SCALE ?? "");
|
|
987
|
+
if (Number.isFinite(raw) && raw > 0)
|
|
988
|
+
return Math.max(0.15, Math.min(1, raw));
|
|
989
|
+
return DEFAULT_DRONE_LAYER_SCALE;
|
|
990
|
+
}
|
|
991
|
+
function scaledPixelSize(size, scale, layout) {
|
|
992
|
+
return {
|
|
993
|
+
width: Math.max(layout.width, Math.floor(size.width * scale)),
|
|
994
|
+
height: Math.max(layout.height, Math.floor(size.height * scale))
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
function imageFps() {
|
|
998
|
+
const raw = Number.parseFloat(process.env.RIG_OPENTUI_IMAGE_FPS ?? "");
|
|
999
|
+
if (Number.isFinite(raw) && raw > 0)
|
|
1000
|
+
return Math.max(10, Math.min(60, raw));
|
|
1001
|
+
return DEFAULT_IMAGE_FPS;
|
|
1002
|
+
}
|
|
1003
|
+
function rgbaWord(color) {
|
|
1004
|
+
return (color[3] << 24 | color[2] << 16 | color[1] << 8 | color[0]) >>> 0;
|
|
1005
|
+
}
|
|
1006
|
+
function fillRgba(data, color, word = rgbaWord(color)) {
|
|
1007
|
+
if (data.byteOffset % 4 === 0 && data.byteLength % 4 === 0) {
|
|
1008
|
+
new Uint32Array(data.buffer, data.byteOffset, data.byteLength / 4).fill(word);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
for (let i = 0;i < data.length; i += 4) {
|
|
1012
|
+
data[i] = color[0];
|
|
1013
|
+
data[i + 1] = color[1];
|
|
1014
|
+
data[i + 2] = color[2];
|
|
1015
|
+
data[i + 3] = color[3];
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function makeCrcTable() {
|
|
1019
|
+
const table = new Uint32Array(256);
|
|
1020
|
+
for (let n = 0;n < 256; n += 1) {
|
|
1021
|
+
let c = n;
|
|
1022
|
+
for (let k = 0;k < 8; k += 1)
|
|
1023
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
1024
|
+
table[n] = c >>> 0;
|
|
1025
|
+
}
|
|
1026
|
+
return table;
|
|
1027
|
+
}
|
|
1028
|
+
function crc32(bytes) {
|
|
1029
|
+
let c = 4294967295;
|
|
1030
|
+
for (let i = 0;i < bytes.length; i += 1)
|
|
1031
|
+
c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
|
|
1032
|
+
return (c ^ 4294967295) >>> 0;
|
|
1033
|
+
}
|
|
1034
|
+
function chunk(type, data) {
|
|
1035
|
+
const out = new Uint8Array(12 + data.length);
|
|
1036
|
+
const view = new DataView(out.buffer);
|
|
1037
|
+
view.setUint32(0, data.length);
|
|
1038
|
+
out[4] = type.charCodeAt(0);
|
|
1039
|
+
out[5] = type.charCodeAt(1);
|
|
1040
|
+
out[6] = type.charCodeAt(2);
|
|
1041
|
+
out[7] = type.charCodeAt(3);
|
|
1042
|
+
out.set(data, 8);
|
|
1043
|
+
view.setUint32(8 + data.length, crc32(out.subarray(4, 8 + data.length)));
|
|
1044
|
+
return out;
|
|
1045
|
+
}
|
|
1046
|
+
function encodePng(width, height, rgba) {
|
|
1047
|
+
const ihdr = new Uint8Array(13);
|
|
1048
|
+
const ihdrView = new DataView(ihdr.buffer);
|
|
1049
|
+
ihdrView.setUint32(0, width);
|
|
1050
|
+
ihdrView.setUint32(4, height);
|
|
1051
|
+
ihdr[8] = 8;
|
|
1052
|
+
ihdr[9] = 6;
|
|
1053
|
+
ihdr[10] = 0;
|
|
1054
|
+
ihdr[11] = 0;
|
|
1055
|
+
ihdr[12] = 0;
|
|
1056
|
+
const stride = width * 4;
|
|
1057
|
+
const raw = new Uint8Array((stride + 1) * height);
|
|
1058
|
+
for (let y = 0;y < height; y += 1) {
|
|
1059
|
+
const rawOffset = y * (stride + 1);
|
|
1060
|
+
raw[rawOffset] = 0;
|
|
1061
|
+
raw.set(rgba.subarray(y * stride, y * stride + stride), rawOffset + 1);
|
|
1062
|
+
}
|
|
1063
|
+
const compressed = deflateSync(raw, { level: ZLIB_LEVEL });
|
|
1064
|
+
return Buffer.concat([
|
|
1065
|
+
Buffer.from(PNG_SIGNATURE),
|
|
1066
|
+
Buffer.from(chunk("IHDR", ihdr)),
|
|
1067
|
+
Buffer.from(chunk("IDAT", compressed)),
|
|
1068
|
+
Buffer.from(chunk("IEND", new Uint8Array(0)))
|
|
1069
|
+
]);
|
|
1070
|
+
}
|
|
1071
|
+
function kittyImageSequence(base64, options) {
|
|
1072
|
+
const params = [`a=T`, `f=${options.format}`, `q=2`, `C=1`, `c=${options.columns}`, `r=${options.rows}`, `i=${options.imageId}`, `z=${options.zIndex}`];
|
|
1073
|
+
if (options.format === 32) {
|
|
1074
|
+
params.push(`s=${options.pixelWidth ?? 1}`, `v=${options.pixelHeight ?? 1}`);
|
|
1075
|
+
if (options.compressed)
|
|
1076
|
+
params.push("o=z");
|
|
1077
|
+
}
|
|
1078
|
+
if (base64.length <= KITTY_CHUNK_SIZE)
|
|
1079
|
+
return `\x1B_G${params.join(",")};${base64}\x1B\\`;
|
|
1080
|
+
const chunks = [];
|
|
1081
|
+
for (let offset = 0;offset < base64.length; offset += KITTY_CHUNK_SIZE) {
|
|
1082
|
+
const part = base64.slice(offset, offset + KITTY_CHUNK_SIZE);
|
|
1083
|
+
const last = offset + KITTY_CHUNK_SIZE >= base64.length;
|
|
1084
|
+
chunks.push(`\x1B_G${offset === 0 ? `${params.join(",")},` : ""}m=${last ? 0 : 1};${part}\x1B\\`);
|
|
1085
|
+
}
|
|
1086
|
+
return chunks.join("");
|
|
1087
|
+
}
|
|
1088
|
+
function isModernImageTerminal(protocol) {
|
|
1089
|
+
return protocol === "kitty";
|
|
1090
|
+
}
|
|
1091
|
+
function clampByte(value) {
|
|
1092
|
+
return Math.max(0, Math.min(255, Math.round(value)));
|
|
1093
|
+
}
|
|
1094
|
+
function blendOver(data, index, color, alphaScale = 1) {
|
|
1095
|
+
const alpha = color[3] / 255 * alphaScale;
|
|
1096
|
+
const inv = 1 - alpha;
|
|
1097
|
+
data[index] = clampByte(color[0] * alpha + data[index] * inv);
|
|
1098
|
+
data[index + 1] = clampByte(color[1] * alpha + data[index + 1] * inv);
|
|
1099
|
+
data[index + 2] = clampByte(color[2] * alpha + data[index + 2] * inv);
|
|
1100
|
+
data[index + 3] = 255;
|
|
1101
|
+
}
|
|
1102
|
+
function drawDisc(data, width, height, cx, cy, radius, color, alpha = 1) {
|
|
1103
|
+
const minX = Math.max(0, Math.floor(cx - radius));
|
|
1104
|
+
const maxX = Math.min(width - 1, Math.ceil(cx + radius));
|
|
1105
|
+
const minY = Math.max(0, Math.floor(cy - radius));
|
|
1106
|
+
const maxY = Math.min(height - 1, Math.ceil(cy + radius));
|
|
1107
|
+
for (let y = minY;y <= maxY; y += 1) {
|
|
1108
|
+
for (let x = minX;x <= maxX; x += 1) {
|
|
1109
|
+
const dx = x + 0.5 - cx;
|
|
1110
|
+
const dy = y + 0.5 - cy;
|
|
1111
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
1112
|
+
if (d > radius)
|
|
1113
|
+
continue;
|
|
1114
|
+
const edge = Math.max(0, Math.min(1, radius - d));
|
|
1115
|
+
blendOver(data, (y * width + x) * 4, color, alpha * Math.min(1, 0.35 + edge));
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function drawGlow2(data, width, height, cx, cy, radius, color, alpha = 0.18) {
|
|
1120
|
+
const minX = Math.max(0, Math.floor(cx - radius));
|
|
1121
|
+
const maxX = Math.min(width - 1, Math.ceil(cx + radius));
|
|
1122
|
+
const minY = Math.max(0, Math.floor(cy - radius));
|
|
1123
|
+
const maxY = Math.min(height - 1, Math.ceil(cy + radius));
|
|
1124
|
+
for (let y = minY;y <= maxY; y += 1) {
|
|
1125
|
+
for (let x = minX;x <= maxX; x += 1) {
|
|
1126
|
+
const dx = x + 0.5 - cx;
|
|
1127
|
+
const dy = y + 0.5 - cy;
|
|
1128
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
1129
|
+
if (d > radius)
|
|
1130
|
+
continue;
|
|
1131
|
+
const falloff = Math.pow(1 - d / Math.max(1, radius), 2.4);
|
|
1132
|
+
blendOver(data, (y * width + x) * 4, color, alpha * falloff);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function drawLine2(data, width, height, x0, y0, x1, y1, color, alpha = 0.5, thickness = 1.4) {
|
|
1137
|
+
const dx = x1 - x0;
|
|
1138
|
+
const dy = y1 - y0;
|
|
1139
|
+
const steps = Math.max(1, Math.ceil(Math.hypot(dx, dy) * 1.2));
|
|
1140
|
+
for (let i = 0;i <= steps; i += 1) {
|
|
1141
|
+
const t2 = i / steps;
|
|
1142
|
+
drawDisc(data, width, height, x0 + dx * t2, y0 + dy * t2, thickness, color, alpha);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function drawDrone(data, width, height, x, y, scale, tick, color) {
|
|
1146
|
+
const wobble = Math.sin(tick * 0.055) * scale * 0.35;
|
|
1147
|
+
const cy = y + wobble;
|
|
1148
|
+
drawGlow2(data, width, height, x, cy, scale * 7, color, 0.18);
|
|
1149
|
+
drawDisc(data, width, height, x, cy, scale * 1.45, color, 0.92);
|
|
1150
|
+
drawDisc(data, width, height, x, cy, scale * 0.55, [235, 255, 176, 255], 0.85);
|
|
1151
|
+
const arm = scale * 4.1;
|
|
1152
|
+
const rotor = scale * 1.6;
|
|
1153
|
+
const arms = [[-arm, -arm * 0.72], [arm, -arm * 0.72], [-arm, arm * 0.72], [arm, arm * 0.72]];
|
|
1154
|
+
for (const [dx, dy] of arms) {
|
|
1155
|
+
drawLine2(data, width, height, x, cy, x + dx, cy + dy, color, 0.42, Math.max(1, scale * 0.22));
|
|
1156
|
+
drawGlow2(data, width, height, x + dx, cy + dy, rotor * 2.6, color, 0.1);
|
|
1157
|
+
drawDisc(data, width, height, x + dx, cy + dy, rotor, color, 0.34 + 0.12 * Math.sin(tick * 0.15));
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function roundRectAlpha(px, py, x, y, w, h, r) {
|
|
1161
|
+
const rx = Math.max(x + r, Math.min(px, x + w - r));
|
|
1162
|
+
const ry = Math.max(y + r, Math.min(py, y + h - r));
|
|
1163
|
+
const dx = px - rx;
|
|
1164
|
+
const dy = py - ry;
|
|
1165
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1166
|
+
return Math.max(0, Math.min(1, r + 0.75 - dist));
|
|
1167
|
+
}
|
|
1168
|
+
function panelRect2(layout, panel, size) {
|
|
1169
|
+
const cellW = size.width / layout.width;
|
|
1170
|
+
const cellH = size.height / layout.height;
|
|
1171
|
+
const leftCells = layout.centerLeft + (panel.left ?? 0);
|
|
1172
|
+
const topCells = layout.centerTop + panel.top;
|
|
1173
|
+
const widthCells = panel.width ?? layout.centerWidth;
|
|
1174
|
+
const heightCells = panel.height;
|
|
1175
|
+
const left = Math.round(leftCells * cellW);
|
|
1176
|
+
const top = Math.round(topCells * cellH);
|
|
1177
|
+
const width = Math.round(widthCells * cellW);
|
|
1178
|
+
const height = Math.round(heightCells * cellH);
|
|
1179
|
+
return width > 8 && height > 8 ? { left, top, width, height } : null;
|
|
1180
|
+
}
|
|
1181
|
+
function downsampleRegion(data, canvasW, canvasH, rect, scale) {
|
|
1182
|
+
const w = Math.max(1, Math.ceil(rect.width / scale));
|
|
1183
|
+
const h = Math.max(1, Math.ceil(rect.height / scale));
|
|
1184
|
+
const out = new Uint8ClampedArray(w * h * 3);
|
|
1185
|
+
for (let y = 0;y < h; y += 1) {
|
|
1186
|
+
for (let x = 0;x < w; x += 1) {
|
|
1187
|
+
let r = 0, g = 0, b = 0, count = 0;
|
|
1188
|
+
const sx0 = rect.left + x * scale;
|
|
1189
|
+
const sy0 = rect.top + y * scale;
|
|
1190
|
+
for (let yy = 0;yy < scale; yy += 1) {
|
|
1191
|
+
const sy = sy0 + yy;
|
|
1192
|
+
if (sy < 0 || sy >= canvasH)
|
|
1193
|
+
continue;
|
|
1194
|
+
for (let xx = 0;xx < scale; xx += 1) {
|
|
1195
|
+
const sx = sx0 + xx;
|
|
1196
|
+
if (sx < 0 || sx >= canvasW)
|
|
1197
|
+
continue;
|
|
1198
|
+
const i = (sy * canvasW + sx) * 4;
|
|
1199
|
+
r += data[i];
|
|
1200
|
+
g += data[i + 1];
|
|
1201
|
+
b += data[i + 2];
|
|
1202
|
+
count += 1;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const oi = (y * w + x) * 3;
|
|
1206
|
+
out[oi] = count ? r / count : BG[0];
|
|
1207
|
+
out[oi + 1] = count ? g / count : BG[1];
|
|
1208
|
+
out[oi + 2] = count ? b / count : BG[2];
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return { pixels: out, width: w, height: h };
|
|
1212
|
+
}
|
|
1213
|
+
function blurRgb(buffer, width, height, radius) {
|
|
1214
|
+
const tmp = new Uint8ClampedArray(buffer.length);
|
|
1215
|
+
const out = new Uint8ClampedArray(buffer.length);
|
|
1216
|
+
const window = radius * 2 + 1;
|
|
1217
|
+
for (let y = 0;y < height; y += 1) {
|
|
1218
|
+
let r = 0, g = 0, b = 0;
|
|
1219
|
+
for (let x = -radius;x <= radius; x += 1) {
|
|
1220
|
+
const sx = Math.max(0, Math.min(width - 1, x));
|
|
1221
|
+
const i = (y * width + sx) * 3;
|
|
1222
|
+
r += buffer[i];
|
|
1223
|
+
g += buffer[i + 1];
|
|
1224
|
+
b += buffer[i + 2];
|
|
1225
|
+
}
|
|
1226
|
+
for (let x = 0;x < width; x += 1) {
|
|
1227
|
+
const oi = (y * width + x) * 3;
|
|
1228
|
+
tmp[oi] = r / window;
|
|
1229
|
+
tmp[oi + 1] = g / window;
|
|
1230
|
+
tmp[oi + 2] = b / window;
|
|
1231
|
+
const removeX = Math.max(0, x - radius);
|
|
1232
|
+
const addX = Math.min(width - 1, x + radius + 1);
|
|
1233
|
+
const ri = (y * width + removeX) * 3;
|
|
1234
|
+
const ai = (y * width + addX) * 3;
|
|
1235
|
+
r += tmp.length ? buffer[ai] - buffer[ri] : 0;
|
|
1236
|
+
g += tmp.length ? buffer[ai + 1] - buffer[ri + 1] : 0;
|
|
1237
|
+
b += tmp.length ? buffer[ai + 2] - buffer[ri + 2] : 0;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
for (let x = 0;x < width; x += 1) {
|
|
1241
|
+
let r = 0, g = 0, b = 0;
|
|
1242
|
+
for (let y = -radius;y <= radius; y += 1) {
|
|
1243
|
+
const sy = Math.max(0, Math.min(height - 1, y));
|
|
1244
|
+
const i = (sy * width + x) * 3;
|
|
1245
|
+
r += tmp[i];
|
|
1246
|
+
g += tmp[i + 1];
|
|
1247
|
+
b += tmp[i + 2];
|
|
1248
|
+
}
|
|
1249
|
+
for (let y = 0;y < height; y += 1) {
|
|
1250
|
+
const oi = (y * width + x) * 3;
|
|
1251
|
+
out[oi] = r / window;
|
|
1252
|
+
out[oi + 1] = g / window;
|
|
1253
|
+
out[oi + 2] = b / window;
|
|
1254
|
+
const removeY = Math.max(0, y - radius);
|
|
1255
|
+
const addY = Math.min(height - 1, y + radius + 1);
|
|
1256
|
+
const ri = (removeY * width + x) * 3;
|
|
1257
|
+
const ai = (addY * width + x) * 3;
|
|
1258
|
+
r += tmp[ai] - tmp[ri];
|
|
1259
|
+
g += tmp[ai + 1] - tmp[ri + 1];
|
|
1260
|
+
b += tmp[ai + 2] - tmp[ri + 2];
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return out;
|
|
1264
|
+
}
|
|
1265
|
+
function panelMaskKey(width, height, rect, headerPx, blurScale) {
|
|
1266
|
+
return `${width}x${height}:${rect.left},${rect.top},${rect.width},${rect.height}:${Math.round(headerPx * 100) / 100}:${blurScale}`;
|
|
1267
|
+
}
|
|
1268
|
+
function getPanelMask(width, height, rect, headerPx, blurScale) {
|
|
1269
|
+
const key = panelMaskKey(width, height, rect, headerPx, blurScale);
|
|
1270
|
+
const cached = panelMaskCache.get(key);
|
|
1271
|
+
if (cached)
|
|
1272
|
+
return cached;
|
|
1273
|
+
const radius = Math.max(14, Math.min(34, Math.round(Math.min(rect.width, rect.height) * 0.035)));
|
|
1274
|
+
const sampledWidth = Math.max(1, Math.ceil(rect.width / blurScale));
|
|
1275
|
+
const sampledHeight = Math.max(1, Math.ceil(rect.height / blurScale));
|
|
1276
|
+
const x0 = Math.max(0, rect.left);
|
|
1277
|
+
const y0 = Math.max(0, rect.top);
|
|
1278
|
+
const x1 = Math.min(width - 1, rect.left + rect.width - 1);
|
|
1279
|
+
const y1 = Math.min(height - 1, rect.top + rect.height - 1);
|
|
1280
|
+
const indices = [];
|
|
1281
|
+
const sampleIndices = [];
|
|
1282
|
+
const panelAlpha = [];
|
|
1283
|
+
const edgeAlpha = [];
|
|
1284
|
+
const headerFlags = [];
|
|
1285
|
+
const dividerIndices = [];
|
|
1286
|
+
for (let y = y0;y <= y1; y += 1) {
|
|
1287
|
+
for (let x = x0;x <= x1; x += 1) {
|
|
1288
|
+
const a = roundRectAlpha(x + 0.5, y + 0.5, rect.left, rect.top, rect.width, rect.height, radius);
|
|
1289
|
+
if (a <= 0)
|
|
1290
|
+
continue;
|
|
1291
|
+
const edge = roundRectAlpha(x + 0.5, y + 0.5, rect.left + 1.2, rect.top + 1.2, rect.width - 2.4, rect.height - 2.4, Math.max(1, radius - 1.2));
|
|
1292
|
+
const sx = Math.max(0, Math.min(sampledWidth - 1, Math.floor((x - rect.left) / blurScale)));
|
|
1293
|
+
const sy = Math.max(0, Math.min(sampledHeight - 1, Math.floor((y - rect.top) / blurScale)));
|
|
1294
|
+
indices.push((y * width + x) * 4);
|
|
1295
|
+
sampleIndices.push((sy * sampledWidth + sx) * 3);
|
|
1296
|
+
panelAlpha.push(a);
|
|
1297
|
+
edgeAlpha.push(edge < 0.96 ? Math.min(1, (1 - edge) * 0.7) : 0);
|
|
1298
|
+
headerFlags.push(y < rect.top + headerPx ? 1 : 0);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
const dividerY = Math.round(rect.top + headerPx);
|
|
1302
|
+
if (dividerY > y0 && dividerY < y1) {
|
|
1303
|
+
for (let x = x0 + radius;x <= x1 - radius; x += 1) {
|
|
1304
|
+
dividerIndices.push((dividerY * width + x) * 4);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const mask = {
|
|
1308
|
+
key,
|
|
1309
|
+
indices: Uint32Array.from(indices),
|
|
1310
|
+
sampleIndices: Uint32Array.from(sampleIndices),
|
|
1311
|
+
panelAlpha: Float32Array.from(panelAlpha),
|
|
1312
|
+
edgeAlpha: Float32Array.from(edgeAlpha),
|
|
1313
|
+
headerFlags: Uint8Array.from(headerFlags),
|
|
1314
|
+
dividerIndices: Uint32Array.from(dividerIndices)
|
|
1315
|
+
};
|
|
1316
|
+
panelMaskCache.set(key, mask);
|
|
1317
|
+
while (panelMaskCache.size > PANEL_MASK_CACHE_LIMIT) {
|
|
1318
|
+
const oldest = panelMaskCache.keys().next().value;
|
|
1319
|
+
if (!oldest)
|
|
1320
|
+
break;
|
|
1321
|
+
panelMaskCache.delete(oldest);
|
|
1322
|
+
}
|
|
1323
|
+
return mask;
|
|
1324
|
+
}
|
|
1325
|
+
function compositePanel(data, width, height, rect, headerPx) {
|
|
1326
|
+
const blurScale = PANEL_BLUR_SCALE;
|
|
1327
|
+
const sampled = downsampleRegion(data, width, height, rect, blurScale);
|
|
1328
|
+
const blurred = blurRgb(sampled.pixels, sampled.width, sampled.height, 4);
|
|
1329
|
+
const mask = getPanelMask(width, height, rect, headerPx, blurScale);
|
|
1330
|
+
for (let i = 0;i < mask.indices.length; i += 1) {
|
|
1331
|
+
const idx = mask.indices[i];
|
|
1332
|
+
const sampleIdx = mask.sampleIndices[i];
|
|
1333
|
+
data[idx] = blurred[sampleIdx];
|
|
1334
|
+
data[idx + 1] = blurred[sampleIdx + 1];
|
|
1335
|
+
data[idx + 2] = blurred[sampleIdx + 2];
|
|
1336
|
+
data[idx + 3] = 255;
|
|
1337
|
+
blendOver(data, idx, mask.headerFlags[i] ? PANEL_HEADER : PANEL, mask.panelAlpha[i]);
|
|
1338
|
+
const edge = mask.edgeAlpha[i];
|
|
1339
|
+
if (edge > 0)
|
|
1340
|
+
blendOver(data, idx, PANEL_LINE2, edge);
|
|
1341
|
+
}
|
|
1342
|
+
for (let i = 0;i < mask.dividerIndices.length; i += 1) {
|
|
1343
|
+
blendOver(data, mask.dividerIndices[i], PANEL_LINE2, 0.28);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function visualSceneKind(scene) {
|
|
1347
|
+
if (scene === "tasks")
|
|
1348
|
+
return "loop";
|
|
1349
|
+
if (scene === "help")
|
|
1350
|
+
return "carrier";
|
|
1351
|
+
return "dispatch";
|
|
1352
|
+
}
|
|
1353
|
+
function seededUnit(seed) {
|
|
1354
|
+
let value = seed >>> 0;
|
|
1355
|
+
value = value * 1664525 + 1013904223 >>> 0;
|
|
1356
|
+
return value / 4294967295;
|
|
1357
|
+
}
|
|
1358
|
+
function sceneCore(width, height, kind) {
|
|
1359
|
+
if (kind === "dispatch")
|
|
1360
|
+
return { x: width * 0.3, y: height * 0.45 };
|
|
1361
|
+
if (kind === "carrier")
|
|
1362
|
+
return { x: width * 0.5, y: height * 0.54 };
|
|
1363
|
+
return { x: width * 0.09, y: height * 0.56 };
|
|
1364
|
+
}
|
|
1365
|
+
function dispatchLane(width, index, count) {
|
|
1366
|
+
const a = seededUnit(28672 + index * 101);
|
|
1367
|
+
const b = seededUnit(36864 + index * 131);
|
|
1368
|
+
const c = seededUnit(45056 + index * 151);
|
|
1369
|
+
const ang = (-0.9 + 1.8 * (index / Math.max(1, count - 1))) * (Math.PI * 0.5);
|
|
1370
|
+
return { ang, len: width * (0.32 + a * 0.42), curve: (b - 0.5) * 0.7, phase: c * Math.PI * 2 };
|
|
1371
|
+
}
|
|
1372
|
+
function lanePoint(width, height, lane, p, kind = "dispatch") {
|
|
1373
|
+
const core = sceneCore(width, height, kind);
|
|
1374
|
+
const dist = lane.len * p;
|
|
1375
|
+
const bend = Math.sin(p * Math.PI) * lane.curve * Math.min(width, height) * 0.035;
|
|
1376
|
+
const ux = Math.cos(lane.ang);
|
|
1377
|
+
const uy = Math.sin(lane.ang);
|
|
1378
|
+
return { x: core.x + ux * dist + -uy * bend, y: core.y + uy * dist + ux * bend };
|
|
1379
|
+
}
|
|
1380
|
+
function drawMarketingRails(data, width, height, kind, tick) {
|
|
1381
|
+
const ys = kind === "loop" ? [0.18, 0.5, 0.82] : kind === "carrier" ? [0.24, 0.54, 0.82] : [0.18, 0.58, 0.82];
|
|
1382
|
+
ys.forEach((ratio, index) => {
|
|
1383
|
+
const y = height * ratio + Math.sin(tick * 0.018 + index) * Math.max(2, height * 0.0025);
|
|
1384
|
+
const color = index === 2 ? CYAN : index === 1 ? LIME : LIME_DIM;
|
|
1385
|
+
drawLine2(data, width, height, width * 0.06, y, width * 0.94, y + Math.sin(index) * height * 0.002, color, index === 1 ? 0.11 : 0.07, Math.max(0.8, height * 0.0012));
|
|
1386
|
+
for (let p = (tick * 0.004 + index * 0.21) % 1;p < 1; p += 0.18 + index * 0.02) {
|
|
1387
|
+
const x = width * (0.06 + 0.88 * p);
|
|
1388
|
+
drawDisc(data, width, height, x, y, Math.max(1.2, height * (0.0017 + index * 0.0004)), color, 0.24);
|
|
1389
|
+
drawGlow2(data, width, height, x, y, Math.max(5, height * 0.006), color, 0.035);
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
function drawDispatchScene(data, width, height, tick) {
|
|
1394
|
+
const core = sceneCore(width, height, "dispatch");
|
|
1395
|
+
const min = Math.min(width, height);
|
|
1396
|
+
drawGlow2(data, width, height, core.x, core.y, min * 0.085, LIME, 0.12);
|
|
1397
|
+
drawDisc(data, width, height, core.x, core.y, min * 0.0065, LIME, 0.85);
|
|
1398
|
+
const laneCount = width < 1000 ? 7 : 12;
|
|
1399
|
+
const lanes = Array.from({ length: laneCount }, (_, index) => dispatchLane(width, index, laneCount));
|
|
1400
|
+
lanes.forEach((lane) => {
|
|
1401
|
+
let prev = lanePoint(width, height, lane, 0);
|
|
1402
|
+
for (let step = 1;step <= 32; step += 1) {
|
|
1403
|
+
const next = lanePoint(width, height, lane, step / 32);
|
|
1404
|
+
drawLine2(data, width, height, prev.x, prev.y, next.x, next.y, lane.phase > Math.PI ? CYAN : LIME_DIM, 0.045, Math.max(0.7, min * 0.0009));
|
|
1405
|
+
prev = next;
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
const packets = width < 1000 ? 70 : 150;
|
|
1409
|
+
for (let i = 0;i < packets; i += 1) {
|
|
1410
|
+
const lane = lanes[i % lanes.length];
|
|
1411
|
+
const speed = 0.003 + seededUnit(12288 + i * 17) * 0.009;
|
|
1412
|
+
const p = (seededUnit(16384 + i * 19) + tick * speed) % 1;
|
|
1413
|
+
const pt = lanePoint(width, height, lane, p);
|
|
1414
|
+
const wobble = (seededUnit(20480 + i * 23) - 0.5) * min * 0.012;
|
|
1415
|
+
const fade = p < 0.1 ? p / 0.1 : p > 0.88 ? (1 - p) / 0.12 : 1;
|
|
1416
|
+
const color = seededUnit(24576 + i * 29) < 0.22 ? CYAN : LIME_DIM;
|
|
1417
|
+
const y = pt.y + wobble * Math.sin(p * Math.PI * 2);
|
|
1418
|
+
drawLine2(data, width, height, pt.x - min * 0.012, y - min * 0.002, pt.x, y, color, 0.08 * fade, Math.max(0.7, min * 0.0008));
|
|
1419
|
+
drawDisc(data, width, height, pt.x, y, Math.max(1.3, min * 0.0022), color, 0.5 * fade);
|
|
1420
|
+
drawGlow2(data, width, height, pt.x, y, Math.max(4, min * 0.008), color, 0.045 * fade);
|
|
1421
|
+
}
|
|
1422
|
+
for (let i = 0;i < (width < 1000 ? 4 : 8); i += 1) {
|
|
1423
|
+
const lane = lanes[i % lanes.length];
|
|
1424
|
+
const p = (tick * 0.002 + i * 0.13) % 1;
|
|
1425
|
+
const pt = lanePoint(width, height, lane, p);
|
|
1426
|
+
drawDrone(data, width, height, pt.x, pt.y + Math.sin(tick * 0.04 + i) * min * 0.004, min * (0.006 + i % 2 * 0.0015), tick + i * 11, i % 3 === 0 ? CYAN : LIME_DIM);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
function drawLoopScene(data, width, height, tick) {
|
|
1430
|
+
const min = Math.min(width, height);
|
|
1431
|
+
const y = height * 0.56;
|
|
1432
|
+
const x0 = width * 0.09;
|
|
1433
|
+
const x1 = width * 0.91;
|
|
1434
|
+
drawLine2(data, width, height, x0, y, x1, y, LIME_DIM, 0.12, Math.max(0.8, min * 0.001));
|
|
1435
|
+
const gates = width < 1000 ? 4 : 5;
|
|
1436
|
+
const active = Math.floor(tick / 16) % gates;
|
|
1437
|
+
for (let i = 0;i < gates; i += 1) {
|
|
1438
|
+
const x = x0 + (x1 - x0) * (i / Math.max(1, gates - 1));
|
|
1439
|
+
const color = i === active ? LIME : i <= active ? LIME_DIM : INK_DIM;
|
|
1440
|
+
drawLine2(data, width, height, x, y - min * 0.05, x, y + min * 0.05, color, i === active ? 0.25 : 0.08, Math.max(0.8, min * 0.001));
|
|
1441
|
+
drawDisc(data, width, height, x, y, i === active ? min * 0.006 : min * 0.0035, color, i === active ? 0.72 : 0.25);
|
|
1442
|
+
drawGlow2(data, width, height, x, y, i === active ? min * 0.018 : min * 0.01, color, i === active ? 0.1 : 0.035);
|
|
1443
|
+
}
|
|
1444
|
+
const packets = width < 1000 ? 36 : 84;
|
|
1445
|
+
for (let i = 0;i < packets; i += 1) {
|
|
1446
|
+
const p = (seededUnit(33024 + i * 13) + tick * (0.0022 + seededUnit(33280 + i * 17) * 0.004)) % 1;
|
|
1447
|
+
const x = x0 + (x1 - x0) * p;
|
|
1448
|
+
const segment = Math.min(gates - 1, Math.floor(p * gates));
|
|
1449
|
+
const color = segment < active ? LIME_DIM : segment === active ? CYAN : INK_DIM;
|
|
1450
|
+
const py = y + (seededUnit(33536 + i * 19) - 0.5) * min * 0.018 * Math.sin(p * Math.PI * 2);
|
|
1451
|
+
drawLine2(data, width, height, x - min * 0.014, py, x, py, color, 0.1, Math.max(0.7, min * 0.0008));
|
|
1452
|
+
drawDisc(data, width, height, x, py, Math.max(1.2, min * 0.002), color, color === INK_DIM ? 0.22 : 0.58);
|
|
1453
|
+
}
|
|
1454
|
+
drawDrone(data, width, height, x0 + (x1 - x0) * (tick * 0.003 % 1), y - min * 0.04, min * 0.0065, tick, LIME_DIM);
|
|
1455
|
+
drawDrone(data, width, height, x0 + (x1 - x0) * ((tick * 0.002 + 0.5) % 1), y + min * 0.036, min * 0.0055, tick + 40, MAGENTA);
|
|
1456
|
+
}
|
|
1457
|
+
function drawCarrierScene(data, width, height, tick) {
|
|
1458
|
+
const min = Math.min(width, height);
|
|
1459
|
+
const core = sceneCore(width, height, "carrier");
|
|
1460
|
+
drawGlow2(data, width, height, core.x, core.y, min * 0.075, LIME, 0.12);
|
|
1461
|
+
drawDisc(data, width, height, core.x, core.y, min * 0.0065, LIME, 0.92);
|
|
1462
|
+
const nodes = width < 1000 ? 5 : 7;
|
|
1463
|
+
const rx = width * 0.36;
|
|
1464
|
+
const ry = height * 0.22;
|
|
1465
|
+
for (let i = 0;i < nodes; i += 1) {
|
|
1466
|
+
const a = i / nodes * Math.PI * 2 - Math.PI / 2 + tick * 0.006;
|
|
1467
|
+
const x = core.x + Math.cos(a) * rx;
|
|
1468
|
+
const y = core.y + Math.sin(a) * ry;
|
|
1469
|
+
const color = i % 2 ? CYAN : LIME_DIM;
|
|
1470
|
+
drawLine2(data, width, height, core.x, core.y, x, y, color, 0.055, Math.max(0.7, min * 0.0008));
|
|
1471
|
+
drawGlow2(data, width, height, x, y, min * 0.018, color, 0.055);
|
|
1472
|
+
drawDisc(data, width, height, x, y, min * 0.0048, color, 0.62);
|
|
1473
|
+
drawDrone(data, width, height, x, y - min * 0.018, min * 0.005, tick + i * 9, color);
|
|
1474
|
+
}
|
|
1475
|
+
const packets = width < 1000 ? 30 : 72;
|
|
1476
|
+
for (let i = 0;i < packets; i += 1) {
|
|
1477
|
+
const lane = i % nodes;
|
|
1478
|
+
const a = lane / nodes * Math.PI * 2 - Math.PI / 2 + tick * 0.006;
|
|
1479
|
+
const nx = core.x + Math.cos(a) * rx;
|
|
1480
|
+
const ny = core.y + Math.sin(a) * ry;
|
|
1481
|
+
const p = (seededUnit(37120 + i * 37) + tick * (0.003 + seededUnit(37376 + i * 43) * 0.009)) % 1;
|
|
1482
|
+
const x = core.x + (nx - core.x) * p;
|
|
1483
|
+
const y = core.y + (ny - core.y) * p;
|
|
1484
|
+
const color = seededUnit(37632 + i * 47) < 0.35 ? CYAN : LIME_DIM;
|
|
1485
|
+
drawDisc(data, width, height, x, y, Math.max(1.2, min * 0.002), color, 0.48);
|
|
1486
|
+
drawGlow2(data, width, height, x, y, min * 0.008, color, 0.035);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function drawBackground(data, width, height, tick, scene) {
|
|
1490
|
+
fillRgba(data, BG, BG_WORD);
|
|
1491
|
+
const kind = visualSceneKind(scene);
|
|
1492
|
+
drawGlow2(data, width, height, width * 0.82, height * 0.04, Math.min(width, height) * 0.22, LIME, 0.035);
|
|
1493
|
+
drawGlow2(data, width, height, width * 0.12, height * 0.18, Math.min(width, height) * 0.17, CYAN, 0.025);
|
|
1494
|
+
drawMarketingRails(data, width, height, kind, tick);
|
|
1495
|
+
if (kind === "loop")
|
|
1496
|
+
drawLoopScene(data, width, height, tick);
|
|
1497
|
+
else if (kind === "carrier")
|
|
1498
|
+
drawCarrierScene(data, width, height, tick);
|
|
1499
|
+
else
|
|
1500
|
+
drawDispatchScene(data, width, height, tick);
|
|
1501
|
+
}
|
|
1502
|
+
function imageVisualFrameKey(input) {
|
|
1503
|
+
const width = Math.max(1, Math.floor(input.pixelSize.width));
|
|
1504
|
+
const height = Math.max(1, Math.floor(input.pixelSize.height));
|
|
1505
|
+
const layer = "layer" in input ? input.layer ?? "full" : "full";
|
|
1506
|
+
return `${layer}:${width}x${height}:${input.layout.width}x${input.layout.height}:${input.scene}:${input.tick}:${input.panels.map((panel) => `${panel.id}:${panel.top}:${panel.height}:${panel.width ?? ""}:${panel.left ?? ""}:${panel.headerHeight ?? ""}`).join("|")}`;
|
|
1507
|
+
}
|
|
1508
|
+
function encodeKittyFrame(width, height, data, input) {
|
|
1509
|
+
if (input.transport === "png") {
|
|
1510
|
+
const png = encodePng(width, height, data);
|
|
1511
|
+
return kittyImageSequence(png.toString("base64"), { columns: input.layout.width, rows: input.layout.height, imageId: input.imageId, zIndex: input.zIndex, format: 100 });
|
|
1512
|
+
}
|
|
1513
|
+
const compressed = deflateSync(data, { level: ZLIB_LEVEL });
|
|
1514
|
+
return kittyImageSequence(compressed.toString("base64"), {
|
|
1515
|
+
columns: input.layout.width,
|
|
1516
|
+
rows: input.layout.height,
|
|
1517
|
+
imageId: input.imageId,
|
|
1518
|
+
zIndex: input.zIndex,
|
|
1519
|
+
format: 32,
|
|
1520
|
+
pixelWidth: width,
|
|
1521
|
+
pixelHeight: height,
|
|
1522
|
+
compressed: true
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
function renderImageVisualFrame(input) {
|
|
1526
|
+
const width = Math.max(1, Math.floor(input.pixelSize.width));
|
|
1527
|
+
const height = Math.max(1, Math.floor(input.pixelSize.height));
|
|
1528
|
+
const byteCount = width * height * 4;
|
|
1529
|
+
if (byteCount > MAX_IMAGE_BYTES)
|
|
1530
|
+
return null;
|
|
1531
|
+
const data = new Uint8ClampedArray(byteCount);
|
|
1532
|
+
drawBackground(data, width, height, input.tick, input.scene);
|
|
1533
|
+
for (const panel of input.panels) {
|
|
1534
|
+
if (panel.chrome !== "ad-terminal")
|
|
1535
|
+
continue;
|
|
1536
|
+
const rect = panelRect2(input.layout, panel, { width, height });
|
|
1537
|
+
if (!rect)
|
|
1538
|
+
continue;
|
|
1539
|
+
const cellH = height / input.layout.height;
|
|
1540
|
+
const headerPx = Math.max(cellH * 3, (panel.headerHeight ?? 3) * cellH);
|
|
1541
|
+
compositePanel(data, width, height, rect, headerPx);
|
|
1542
|
+
}
|
|
1543
|
+
return { key: imageVisualFrameKey(input), sequence: encodeKittyFrame(width, height, data, input) };
|
|
1544
|
+
}
|
|
1545
|
+
function createImageWorker() {
|
|
1546
|
+
if (process.env.RIG_OPENTUI_IMAGE_WORKER === "0")
|
|
1547
|
+
return null;
|
|
1548
|
+
const isTypeScriptRuntime = import.meta.url.endsWith(".ts");
|
|
1549
|
+
const workerUrl = new URL(isTypeScriptRuntime ? "./image-visual-layer-worker.ts" : "./image-visual-layer-worker.js", import.meta.url);
|
|
1550
|
+
try {
|
|
1551
|
+
return new Worker(workerUrl);
|
|
1552
|
+
} catch {
|
|
1553
|
+
if (typeof process.versions.bun === "string") {
|
|
1554
|
+
try {
|
|
1555
|
+
return new Worker("./src/app-opentui/render/image-visual-layer-worker.ts");
|
|
1556
|
+
} catch {}
|
|
1557
|
+
}
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
var RECENT_WRITTEN_LIMIT = 24;
|
|
1562
|
+
var DEFAULT_PANEL_FPS = 24;
|
|
1563
|
+
var DEFAULT_PANEL_LAYER_SCALE = 0.22;
|
|
1564
|
+
var DEFAULT_IMAGE_OUTPUT_BACKLOG_BYTES = 256 * 1024;
|
|
1565
|
+
var DEFAULT_IMAGE_OUTPUT_BYTES_PER_SECOND = 850 * 1024;
|
|
1566
|
+
var MIN_BACKGROUND_BUDGET_BYTES = 24 * 1024;
|
|
1567
|
+
var MIN_PANEL_BUDGET_BYTES = 56 * 1024;
|
|
1568
|
+
function panelFps() {
|
|
1569
|
+
const raw = Number.parseFloat(process.env.RIG_OPENTUI_PANEL_FPS ?? "");
|
|
1570
|
+
if (Number.isFinite(raw) && raw > 0)
|
|
1571
|
+
return Math.max(4, Math.min(60, raw));
|
|
1572
|
+
return DEFAULT_PANEL_FPS;
|
|
1573
|
+
}
|
|
1574
|
+
function panelLayerScale() {
|
|
1575
|
+
const raw = Number.parseFloat(process.env.RIG_OPENTUI_PANEL_SCALE ?? "");
|
|
1576
|
+
if (Number.isFinite(raw) && raw > 0)
|
|
1577
|
+
return Math.max(0.18, Math.min(1, raw));
|
|
1578
|
+
return DEFAULT_PANEL_LAYER_SCALE;
|
|
1579
|
+
}
|
|
1580
|
+
function imageOutputBacklogBytes() {
|
|
1581
|
+
const raw = Number.parseInt(process.env.RIG_OPENTUI_IMAGE_BACKLOG_BYTES ?? "", 10);
|
|
1582
|
+
if (Number.isFinite(raw) && raw > 0)
|
|
1583
|
+
return Math.max(64 * 1024, Math.min(2 * 1024 * 1024, raw));
|
|
1584
|
+
return DEFAULT_IMAGE_OUTPUT_BACKLOG_BYTES;
|
|
1585
|
+
}
|
|
1586
|
+
function imageOutputBytesPerSecond() {
|
|
1587
|
+
const raw = Number.parseInt(process.env.RIG_OPENTUI_IMAGE_BYTES_PER_SECOND ?? "", 10);
|
|
1588
|
+
if (Number.isFinite(raw) && raw > 0)
|
|
1589
|
+
return Math.max(128 * 1024, Math.min(2 * 1024 * 1024, raw));
|
|
1590
|
+
return DEFAULT_IMAGE_OUTPUT_BYTES_PER_SECOND;
|
|
1591
|
+
}
|
|
1592
|
+
function pendingLayerKey(key) {
|
|
1593
|
+
return key.split(":", 1)[0] || "full";
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
class ImageVisualLayer {
|
|
1597
|
+
imageId = allocateImageId();
|
|
1598
|
+
panelImageIds = Array.from({ length: 8 }, () => allocateImageId());
|
|
1599
|
+
panelChromeImageIds = Array.from({ length: 8 }, () => allocateImageId());
|
|
1600
|
+
stdout;
|
|
1601
|
+
protocol;
|
|
1602
|
+
transport = imageTransport();
|
|
1603
|
+
frameIntervalMs = 1000 / imageFps();
|
|
1604
|
+
panelIntervalMs = 1000 / panelFps();
|
|
1605
|
+
maxOutputBacklogBytes = imageOutputBacklogBytes();
|
|
1606
|
+
outputBytesPerSecond = imageOutputBytesPerSecond();
|
|
1607
|
+
outputBudgetBytes = imageOutputBytesPerSecond();
|
|
1608
|
+
lastBudgetAtMs = Date.now();
|
|
1609
|
+
motionSlot = this.createSlot();
|
|
1610
|
+
panelSlot = this.createSlot();
|
|
1611
|
+
chromeSlot = this.createSlot();
|
|
1612
|
+
pendingFrames = new Map;
|
|
1613
|
+
recentWrittenKeys = [];
|
|
1614
|
+
requestId = 0;
|
|
1615
|
+
lastMotionFrameIndex = -1;
|
|
1616
|
+
lastPanelFrameIndex = -1;
|
|
1617
|
+
lastChromeKey = "";
|
|
1618
|
+
destroyed = false;
|
|
1619
|
+
constructor(stdout = process.stdout) {
|
|
1620
|
+
this.stdout = stdout;
|
|
1621
|
+
this.protocol = getCapabilities().images;
|
|
1622
|
+
if (this.enabled) {
|
|
1623
|
+
this.startWorker(this.motionSlot);
|
|
1624
|
+
this.startWorker(this.panelSlot);
|
|
1625
|
+
this.startWorker(this.chromeSlot);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
get enabled() {
|
|
1629
|
+
return isModernImageTerminal(this.protocol);
|
|
1630
|
+
}
|
|
1631
|
+
clear() {
|
|
1632
|
+
if (this.enabled) {
|
|
1633
|
+
this.stdout.write([this.imageId, ...this.panelImageIds, ...this.panelChromeImageIds].map((id) => deleteKittyImage(id)).join("") + "\x1B[?25h");
|
|
1634
|
+
this.pendingFrames.clear();
|
|
1635
|
+
this.recentWrittenKeys.length = 0;
|
|
1636
|
+
this.resetSlot(this.motionSlot);
|
|
1637
|
+
this.resetSlot(this.panelSlot);
|
|
1638
|
+
this.resetSlot(this.chromeSlot);
|
|
1639
|
+
this.lastMotionFrameIndex = -1;
|
|
1640
|
+
this.lastPanelFrameIndex = -1;
|
|
1641
|
+
this.lastChromeKey = "";
|
|
1642
|
+
this.outputBudgetBytes = this.outputBytesPerSecond;
|
|
1643
|
+
this.lastBudgetAtMs = Date.now();
|
|
1644
|
+
this.motionSlot.clearedThroughRequestId = this.requestId;
|
|
1645
|
+
this.panelSlot.clearedThroughRequestId = this.requestId;
|
|
1646
|
+
this.chromeSlot.clearedThroughRequestId = this.requestId;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
destroy() {
|
|
1650
|
+
if (this.destroyed)
|
|
1651
|
+
return;
|
|
1652
|
+
this.clear();
|
|
1653
|
+
this.destroyed = true;
|
|
1654
|
+
const workers = [this.motionSlot.worker, this.panelSlot.worker, this.chromeSlot.worker];
|
|
1655
|
+
this.motionSlot.worker = null;
|
|
1656
|
+
this.panelSlot.worker = null;
|
|
1657
|
+
this.chromeSlot.worker = null;
|
|
1658
|
+
for (const worker of workers)
|
|
1659
|
+
if (worker)
|
|
1660
|
+
worker.terminate().catch(() => {
|
|
1661
|
+
return;
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
render(input) {
|
|
1665
|
+
if (!this.enabled || this.destroyed)
|
|
1666
|
+
return;
|
|
1667
|
+
const width = Math.max(1, Math.floor(input.pixelSize.width));
|
|
1668
|
+
const height = Math.max(1, Math.floor(input.pixelSize.height));
|
|
1669
|
+
if (width * height * 4 > MAX_IMAGE_BYTES)
|
|
1670
|
+
return;
|
|
1671
|
+
if (this.isOutputBackedUp())
|
|
1672
|
+
return;
|
|
1673
|
+
this.refillOutputBudget();
|
|
1674
|
+
const now = Date.now();
|
|
1675
|
+
const visualTick = Math.floor(now / (1000 / 60));
|
|
1676
|
+
const useDynamicSplit = Boolean(this.motionSlot.worker && this.panelSlot.worker && this.chromeSlot.worker && this.transport === "png" && process.env.RIG_OPENTUI_DYNAMIC_SPLIT !== "0");
|
|
1677
|
+
if (!useDynamicSplit) {
|
|
1678
|
+
const frameIndex = Math.floor(now / this.frameIntervalMs);
|
|
1679
|
+
if (!input.force && frameIndex === this.lastMotionFrameIndex)
|
|
1680
|
+
return;
|
|
1681
|
+
this.scheduleRender(this.motionSlot, { ...input, tick: visualTick, layer: "full" }, this.imageId, -10, input.force);
|
|
1682
|
+
this.lastMotionFrameIndex = frameIndex;
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const motionFrameIndex = Math.floor(now / this.frameIntervalMs);
|
|
1686
|
+
if ((input.force || motionFrameIndex !== this.lastMotionFrameIndex) && this.outputBudgetBytes >= MIN_BACKGROUND_BUDGET_BYTES) {
|
|
1687
|
+
this.scheduleRender(this.motionSlot, { ...input, pixelSize: scaledPixelSize(input.pixelSize, droneLayerScale(), input.layout), panels: [], tick: visualTick, layer: "background" }, this.imageId, -30, input.force);
|
|
1688
|
+
this.lastMotionFrameIndex = motionFrameIndex;
|
|
1689
|
+
}
|
|
1690
|
+
const panelFrameIndex = Math.floor(now / this.panelIntervalMs);
|
|
1691
|
+
if ((input.force || panelFrameIndex !== this.lastPanelFrameIndex) && this.outputBudgetBytes >= MIN_PANEL_BUDGET_BYTES) {
|
|
1692
|
+
this.scheduleRender(this.panelSlot, { ...input, pixelSize: scaledPixelSize(input.pixelSize, panelLayerScale(), input.layout), tick: visualTick, layer: "panel-patches" }, this.imageId, -12, input.force, this.panelImageIds);
|
|
1693
|
+
this.lastPanelFrameIndex = panelFrameIndex;
|
|
1694
|
+
}
|
|
1695
|
+
const chromeInput = { ...input, tick: 0, layer: "panel-chrome" };
|
|
1696
|
+
const chromeKey = imageVisualFrameKey(chromeInput);
|
|
1697
|
+
if (input.force || chromeKey !== this.lastChromeKey) {
|
|
1698
|
+
this.scheduleRender(this.chromeSlot, chromeInput, this.imageId, -8, input.force, this.panelChromeImageIds);
|
|
1699
|
+
this.lastChromeKey = chromeKey;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
flush() {
|
|
1703
|
+
if (this.pendingFrames.size === 0)
|
|
1704
|
+
return false;
|
|
1705
|
+
this.refillOutputBudget();
|
|
1706
|
+
if (this.isOutputBackedUp()) {
|
|
1707
|
+
const chrome = this.pendingFrames.get("panel-chrome");
|
|
1708
|
+
this.pendingFrames.clear();
|
|
1709
|
+
if (chrome)
|
|
1710
|
+
this.pendingFrames.set("panel-chrome", chrome);
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
const framePriority = (frame) => {
|
|
1714
|
+
const layer = pendingLayerKey(frame.key);
|
|
1715
|
+
if (layer === "panel-chrome")
|
|
1716
|
+
return 0;
|
|
1717
|
+
if (layer === "background")
|
|
1718
|
+
return 1;
|
|
1719
|
+
if (layer === "panel-patches")
|
|
1720
|
+
return 2;
|
|
1721
|
+
return 2;
|
|
1722
|
+
};
|
|
1723
|
+
const frames = Array.from(this.pendingFrames.values()).sort((a, b) => framePriority(a) - framePriority(b));
|
|
1724
|
+
const prefix = "\x1B[?25l\x1B7\x1B[s\x1B[H";
|
|
1725
|
+
const suffix = "\x1B[u\x1B8";
|
|
1726
|
+
const selected = [];
|
|
1727
|
+
const retained = new Map;
|
|
1728
|
+
let sequence = "";
|
|
1729
|
+
let byteLength = Buffer.byteLength(prefix + suffix);
|
|
1730
|
+
for (const frame of frames) {
|
|
1731
|
+
const candidate = sequence + frame.sequence;
|
|
1732
|
+
const candidateBytes = Buffer.byteLength(prefix + candidate + suffix);
|
|
1733
|
+
if (candidateBytes <= this.outputBudgetBytes) {
|
|
1734
|
+
selected.push(frame);
|
|
1735
|
+
sequence = candidate;
|
|
1736
|
+
byteLength = candidateBytes;
|
|
1737
|
+
} else if (pendingLayerKey(frame.key) === "panel-chrome") {
|
|
1738
|
+
retained.set(pendingLayerKey(frame.key), frame);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
this.pendingFrames.clear();
|
|
1742
|
+
for (const [layer, frame] of retained)
|
|
1743
|
+
this.pendingFrames.set(layer, frame);
|
|
1744
|
+
if (!sequence || selected.length === 0)
|
|
1745
|
+
return false;
|
|
1746
|
+
const payload = `${prefix}${sequence}${suffix}`;
|
|
1747
|
+
for (const frame of selected)
|
|
1748
|
+
this.markWritten(frame.key);
|
|
1749
|
+
this.outputBudgetBytes -= byteLength;
|
|
1750
|
+
this.stdout.cork?.();
|
|
1751
|
+
const accepted = this.stdout.write(payload);
|
|
1752
|
+
this.stdout.uncork?.();
|
|
1753
|
+
return accepted;
|
|
1754
|
+
}
|
|
1755
|
+
createSlot() {
|
|
1756
|
+
return { worker: null, busy: false, queuedRequest: null, inFlightKey: "", queuedKey: "", clearedThroughRequestId: 0 };
|
|
1757
|
+
}
|
|
1758
|
+
resetSlot(slot) {
|
|
1759
|
+
slot.busy = false;
|
|
1760
|
+
slot.queuedRequest = null;
|
|
1761
|
+
slot.inFlightKey = "";
|
|
1762
|
+
slot.queuedKey = "";
|
|
1763
|
+
}
|
|
1764
|
+
markWritten(key) {
|
|
1765
|
+
this.recentWrittenKeys.push(key);
|
|
1766
|
+
while (this.recentWrittenKeys.length > RECENT_WRITTEN_LIMIT)
|
|
1767
|
+
this.recentWrittenKeys.shift();
|
|
1768
|
+
}
|
|
1769
|
+
refillOutputBudget() {
|
|
1770
|
+
const now = Date.now();
|
|
1771
|
+
const elapsedMs = Math.max(0, now - this.lastBudgetAtMs);
|
|
1772
|
+
this.lastBudgetAtMs = now;
|
|
1773
|
+
this.outputBudgetBytes = Math.min(this.outputBytesPerSecond, this.outputBudgetBytes + elapsedMs / 1000 * this.outputBytesPerSecond);
|
|
1774
|
+
}
|
|
1775
|
+
isOutputBackedUp() {
|
|
1776
|
+
return Boolean(this.stdout.writableNeedDrain) || this.stdout.writableLength > this.maxOutputBacklogBytes;
|
|
1777
|
+
}
|
|
1778
|
+
stageFrame(frame) {
|
|
1779
|
+
this.pendingFrames.set(pendingLayerKey(frame.key), frame);
|
|
1780
|
+
}
|
|
1781
|
+
isKnownKey(slot, key) {
|
|
1782
|
+
return this.recentWrittenKeys.includes(key) || Array.from(this.pendingFrames.values()).some((frame) => frame.key === key) || key === slot.inFlightKey || key === slot.queuedKey;
|
|
1783
|
+
}
|
|
1784
|
+
scheduleRender(slot, input, imageId, zIndex, force, imageIds) {
|
|
1785
|
+
const key = imageVisualFrameKey(input);
|
|
1786
|
+
if (!force && this.isKnownKey(slot, key))
|
|
1787
|
+
return;
|
|
1788
|
+
const request = {
|
|
1789
|
+
...input,
|
|
1790
|
+
imageId,
|
|
1791
|
+
...imageIds ? { imageIds } : {},
|
|
1792
|
+
zIndex,
|
|
1793
|
+
transport: this.transport,
|
|
1794
|
+
requestId: ++this.requestId,
|
|
1795
|
+
key
|
|
1796
|
+
};
|
|
1797
|
+
if (slot.worker) {
|
|
1798
|
+
this.enqueueWorkerRender(slot, request);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
this.renderSync(slot, request);
|
|
1802
|
+
}
|
|
1803
|
+
startWorker(slot) {
|
|
1804
|
+
const worker = createImageWorker();
|
|
1805
|
+
if (!worker)
|
|
1806
|
+
return;
|
|
1807
|
+
slot.worker = worker;
|
|
1808
|
+
worker.on("message", (message) => this.handleWorkerMessage(slot, message));
|
|
1809
|
+
worker.once("error", () => this.disableWorker(slot));
|
|
1810
|
+
worker.once("exit", () => this.disableWorker(slot));
|
|
1811
|
+
}
|
|
1812
|
+
disableWorker(slot) {
|
|
1813
|
+
if (this.destroyed)
|
|
1814
|
+
return;
|
|
1815
|
+
slot.worker = null;
|
|
1816
|
+
slot.busy = false;
|
|
1817
|
+
slot.inFlightKey = "";
|
|
1818
|
+
const queued = slot.queuedRequest;
|
|
1819
|
+
slot.queuedRequest = null;
|
|
1820
|
+
slot.queuedKey = "";
|
|
1821
|
+
if (queued?.layer === "full")
|
|
1822
|
+
this.renderSync(slot, queued);
|
|
1823
|
+
}
|
|
1824
|
+
enqueueWorkerRender(slot, request) {
|
|
1825
|
+
if (!slot.worker) {
|
|
1826
|
+
this.renderSync(slot, request);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (slot.busy) {
|
|
1830
|
+
slot.queuedRequest = request;
|
|
1831
|
+
slot.queuedKey = request.key;
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
this.dispatchWorkerRender(slot, request);
|
|
1835
|
+
}
|
|
1836
|
+
dispatchWorkerRender(slot, request) {
|
|
1837
|
+
if (!slot.worker) {
|
|
1838
|
+
this.renderSync(slot, request);
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
slot.busy = true;
|
|
1842
|
+
slot.inFlightKey = request.key;
|
|
1843
|
+
slot.worker.postMessage(request);
|
|
1844
|
+
}
|
|
1845
|
+
handleWorkerMessage(slot, message) {
|
|
1846
|
+
if (this.destroyed || !message || typeof message !== "object")
|
|
1847
|
+
return;
|
|
1848
|
+
const response = message;
|
|
1849
|
+
slot.busy = false;
|
|
1850
|
+
slot.inFlightKey = "";
|
|
1851
|
+
if (response.requestId <= slot.clearedThroughRequestId)
|
|
1852
|
+
return;
|
|
1853
|
+
if (response.error) {
|
|
1854
|
+
this.disableWorker(slot);
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
if (response.sequence && !this.isKnownKey(slot, response.key) && (!this.isOutputBackedUp() || pendingLayerKey(response.key) === "panel-chrome")) {
|
|
1858
|
+
this.stageFrame({ key: response.key, sequence: response.sequence });
|
|
1859
|
+
}
|
|
1860
|
+
const next = slot.queuedRequest;
|
|
1861
|
+
slot.queuedRequest = null;
|
|
1862
|
+
slot.queuedKey = "";
|
|
1863
|
+
if (next && !this.isKnownKey(slot, next.key))
|
|
1864
|
+
this.dispatchWorkerRender(slot, next);
|
|
1865
|
+
}
|
|
1866
|
+
renderSync(slot, request) {
|
|
1867
|
+
const result = renderImageVisualFrame(request);
|
|
1868
|
+
if (!result || this.isKnownKey(slot, result.key) || this.isOutputBackedUp())
|
|
1869
|
+
return;
|
|
1870
|
+
this.stageFrame(result);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
function pixelSizeForRenderer(renderer) {
|
|
1874
|
+
const resolution = renderer.resolution;
|
|
1875
|
+
const base = resolution?.width && resolution?.height ? resolution : { width: renderer.width * 12, height: renderer.height * 24 };
|
|
1876
|
+
const scale = imageResolutionScale();
|
|
1877
|
+
return {
|
|
1878
|
+
width: Math.max(renderer.width, Math.floor(base.width * scale)),
|
|
1879
|
+
height: Math.max(renderer.height, Math.floor(base.height * scale))
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
function forceModernImageVisuals() {
|
|
1883
|
+
return isModernImageTerminal(getCapabilities().images);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// packages/cli/src/app-opentui/render/text.ts
|
|
1887
|
+
import { TextAttributes as TextAttributes2, TextRenderable } from "@opentui/core";
|
|
1888
|
+
function selectableMouseHandler(currentLineRef, onLineMouseDown) {
|
|
1889
|
+
return (event) => {
|
|
1890
|
+
const currentLine = currentLineRef();
|
|
1891
|
+
if (currentLine?.selectableIndex === undefined)
|
|
1892
|
+
return;
|
|
1893
|
+
onLineMouseDown?.(currentLine, event);
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
function createTextLine(renderer, id, top, onLineMouseDown) {
|
|
1897
|
+
let currentLine;
|
|
1898
|
+
const renderable = new TextRenderable(renderer, {
|
|
1899
|
+
id,
|
|
1900
|
+
content: "",
|
|
1901
|
+
position: "absolute",
|
|
1902
|
+
left: 0,
|
|
1903
|
+
top,
|
|
1904
|
+
width: 1,
|
|
1905
|
+
fg: RIG_UI.ink2,
|
|
1906
|
+
selectable: true,
|
|
1907
|
+
onMouseDown: selectableMouseHandler(() => currentLine, onLineMouseDown)
|
|
1908
|
+
});
|
|
1909
|
+
renderable.setRigSceneLine = (line) => {
|
|
1910
|
+
currentLine = line;
|
|
1911
|
+
};
|
|
1912
|
+
return renderable;
|
|
1913
|
+
}
|
|
1914
|
+
function createFlowTextLine(renderer, id, onLineMouseDown) {
|
|
1915
|
+
let currentLine;
|
|
1916
|
+
const renderable = new TextRenderable(renderer, {
|
|
1917
|
+
id,
|
|
1918
|
+
content: "",
|
|
1919
|
+
width: "100%",
|
|
1920
|
+
height: 1,
|
|
1921
|
+
flexShrink: 0,
|
|
1922
|
+
fg: RIG_UI.ink2,
|
|
1923
|
+
selectable: true,
|
|
1924
|
+
onMouseDown: selectableMouseHandler(() => currentLine, onLineMouseDown)
|
|
1925
|
+
});
|
|
1926
|
+
renderable.setRigSceneLine = (line) => {
|
|
1927
|
+
currentLine = line;
|
|
1928
|
+
};
|
|
1929
|
+
return renderable;
|
|
1930
|
+
}
|
|
1931
|
+
function applyTextLine(renderable, line, top, left, width) {
|
|
1932
|
+
renderable.setRigSceneLine?.(line);
|
|
1933
|
+
renderable.top = top;
|
|
1934
|
+
renderable.left = left;
|
|
1935
|
+
renderable.width = width;
|
|
1936
|
+
renderable.content = line?.styledText ?? line?.text ?? "";
|
|
1937
|
+
renderable.fg = line?.fg ?? RIG_UI.ink2;
|
|
1938
|
+
renderable.attributes = line?.bold ? TextAttributes2.BOLD : line?.dim ? TextAttributes2.DIM : 0;
|
|
1939
|
+
}
|
|
1940
|
+
function applyFlowTextLine(renderable, line, width) {
|
|
1941
|
+
renderable.setRigSceneLine?.(line);
|
|
1942
|
+
renderable.visible = Boolean(line);
|
|
1943
|
+
renderable.height = line ? 1 : 0;
|
|
1944
|
+
renderable.width = Math.max(1, width);
|
|
1945
|
+
renderable.content = line?.styledText ?? line?.text ?? "";
|
|
1946
|
+
renderable.fg = line?.fg ?? RIG_UI.ink2;
|
|
1947
|
+
renderable.attributes = line?.bold ? TextAttributes2.BOLD : line?.dim ? TextAttributes2.DIM : 0;
|
|
1948
|
+
}
|
|
1949
|
+
function clearTextLines(renderables, from = 0) {
|
|
1950
|
+
for (let index = from;index < renderables.length; index += 1) {
|
|
1951
|
+
const renderable = renderables[index];
|
|
1952
|
+
renderable.setRigSceneLine?.(undefined);
|
|
1953
|
+
renderable.content = "";
|
|
1954
|
+
renderable.top = -1;
|
|
1955
|
+
renderable.left = 0;
|
|
1956
|
+
renderable.width = 1;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// packages/cli/src/app-opentui/render/panels.ts
|
|
1961
|
+
import { RGBA as RGBA2, ScrollBoxRenderable } from "@opentui/core";
|
|
1962
|
+
var TRANSPARENT2 = RGBA2.fromInts(0, 0, 0, 0);
|
|
1963
|
+
var MAX_PANEL_LINES = 420;
|
|
1964
|
+
function hexToRgba(hex, alpha) {
|
|
1965
|
+
const clean = hex.replace(/^#/, "");
|
|
1966
|
+
const full = clean.length === 3 ? clean.split("").map((part) => `${part}${part}`).join("") : clean;
|
|
1967
|
+
const value = Number.parseInt(full, 16);
|
|
1968
|
+
if (!Number.isFinite(value))
|
|
1969
|
+
return RGBA2.fromInts(14, 15, 17, alpha);
|
|
1970
|
+
return RGBA2.fromInts(value >> 16 & 255, value >> 8 & 255, value & 255, alpha);
|
|
1971
|
+
}
|
|
1972
|
+
function panelBackground(panel) {
|
|
1973
|
+
return hexToRgba(panel.backgroundColor ?? RIG_UI.panel, panel.backgroundAlpha ?? 184);
|
|
1974
|
+
}
|
|
1975
|
+
function panelBorder(panel) {
|
|
1976
|
+
return panel.borderColor ? hexToRgba(panel.borderColor, panel.borderAlpha ?? 54) : TRANSPARENT2;
|
|
1977
|
+
}
|
|
1978
|
+
function createScrollPanelLayer(renderer, id, onLineMouseDown) {
|
|
1979
|
+
const panel = new ScrollBoxRenderable(renderer, {
|
|
1980
|
+
id,
|
|
1981
|
+
position: "absolute",
|
|
1982
|
+
left: 0,
|
|
1983
|
+
top: -2,
|
|
1984
|
+
width: 1,
|
|
1985
|
+
height: 1,
|
|
1986
|
+
backgroundColor: TRANSPARENT2,
|
|
1987
|
+
border: false,
|
|
1988
|
+
shouldFill: true,
|
|
1989
|
+
opacity: 1,
|
|
1990
|
+
zIndex: 6,
|
|
1991
|
+
scrollY: true,
|
|
1992
|
+
scrollX: false,
|
|
1993
|
+
stickyScroll: false,
|
|
1994
|
+
viewportCulling: true,
|
|
1995
|
+
paddingX: 2,
|
|
1996
|
+
paddingY: 1,
|
|
1997
|
+
verticalScrollbarOptions: {
|
|
1998
|
+
showArrows: false,
|
|
1999
|
+
visible: false,
|
|
2000
|
+
width: 1,
|
|
2001
|
+
trackOptions: {
|
|
2002
|
+
backgroundColor: TRANSPARENT2,
|
|
2003
|
+
foregroundColor: hexToRgba(RIG_UI.ink4, 84)
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
horizontalScrollbarOptions: {
|
|
2007
|
+
height: 0,
|
|
2008
|
+
visible: false,
|
|
2009
|
+
trackOptions: {
|
|
2010
|
+
backgroundColor: TRANSPARENT2,
|
|
2011
|
+
foregroundColor: TRANSPARENT2
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
2015
|
+
panel.__rigTextLines = [];
|
|
2016
|
+
for (let index = 0;index < MAX_PANEL_LINES; index += 1) {
|
|
2017
|
+
const line = createFlowTextLine(renderer, `${id}-line-${index}`, onLineMouseDown);
|
|
2018
|
+
panel.__rigTextLines.push(line);
|
|
2019
|
+
panel.add(line);
|
|
2020
|
+
}
|
|
2021
|
+
return panel;
|
|
2022
|
+
}
|
|
2023
|
+
function hidePanel(panel) {
|
|
2024
|
+
panel.visible = false;
|
|
2025
|
+
panel.top = -2;
|
|
2026
|
+
panel.left = 0;
|
|
2027
|
+
panel.width = 1;
|
|
2028
|
+
panel.height = 1;
|
|
2029
|
+
panel.backgroundColor = TRANSPARENT2;
|
|
2030
|
+
panel.border = false;
|
|
2031
|
+
panel.__rigPanelId = undefined;
|
|
2032
|
+
panel.__rigTextLines.forEach((line) => applyFlowTextLine(line, undefined, 1));
|
|
2033
|
+
}
|
|
2034
|
+
function applyScrollPanels(panels, layout, scenePanels) {
|
|
2035
|
+
panels.forEach((renderable, index) => {
|
|
2036
|
+
const panel = scenePanels[index];
|
|
2037
|
+
if (!panel || panel.height <= 0) {
|
|
2038
|
+
hidePanel(renderable);
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
const cardLeft = Math.max(0, Math.min(layout.width - 1, layout.centerLeft + (panel.left ?? 0)));
|
|
2042
|
+
const desiredWidth = panel.width ?? layout.centerWidth;
|
|
2043
|
+
const cardWidth = Math.max(1, Math.min(layout.width - cardLeft, desiredWidth));
|
|
2044
|
+
const cardTop = Math.max(0, Math.min(layout.height - 1, layout.centerTop + panel.top));
|
|
2045
|
+
const cardHeight = Math.max(1, Math.min(layout.height - cardTop, panel.height));
|
|
2046
|
+
const headerHeight = panel.chrome === "ad-terminal" ? Math.max(3, Math.min(cardHeight - 2, panel.headerHeight ?? 3)) : 0;
|
|
2047
|
+
const left = cardLeft + (panel.chrome === "ad-terminal" ? 1 : 0);
|
|
2048
|
+
const top = cardTop + headerHeight + (panel.chrome === "ad-terminal" ? 1 : 0);
|
|
2049
|
+
const width = Math.max(1, Math.min(layout.width - left, cardWidth - (panel.chrome === "ad-terminal" ? 2 : 0)));
|
|
2050
|
+
const height = Math.max(1, Math.min(layout.height - top, cardHeight - headerHeight - (panel.chrome === "ad-terminal" ? 2 : 0)));
|
|
2051
|
+
const paddingX = panel.paddingX ?? (panel.chrome === "ad-terminal" ? 5 : 2);
|
|
2052
|
+
const paddingY = panel.paddingY ?? (panel.chrome === "ad-terminal" ? 2 : 1);
|
|
2053
|
+
const contentWidth = Math.max(1, width - paddingX * 2 - 1);
|
|
2054
|
+
renderable.visible = true;
|
|
2055
|
+
renderable.left = left;
|
|
2056
|
+
renderable.top = top;
|
|
2057
|
+
renderable.width = width;
|
|
2058
|
+
renderable.height = height;
|
|
2059
|
+
renderable.zIndex = panel.zIndex ?? 6;
|
|
2060
|
+
renderable.opacity = panel.opacity ?? 1;
|
|
2061
|
+
renderable.backgroundColor = panel.chrome === "ad-terminal" ? TRANSPARENT2 : panelBackground(panel);
|
|
2062
|
+
renderable.border = panel.chrome === "ad-terminal" ? false : panel.border ?? false;
|
|
2063
|
+
renderable.borderColor = panelBorder(panel);
|
|
2064
|
+
renderable.paddingX = paddingX;
|
|
2065
|
+
renderable.paddingY = paddingY;
|
|
2066
|
+
renderable.stickyScroll = panel.stickyScroll ?? false;
|
|
2067
|
+
renderable.stickyStart = panel.stickyStart;
|
|
2068
|
+
renderable.verticalScrollBar.visible = false;
|
|
2069
|
+
renderable.horizontalScrollBar.visible = false;
|
|
2070
|
+
if (renderable.__rigPanelId !== panel.id) {
|
|
2071
|
+
renderable.__rigPanelId = panel.id;
|
|
2072
|
+
renderable.scrollTo(0);
|
|
2073
|
+
}
|
|
2074
|
+
renderable.__rigTextLines.forEach((line, lineIndex) => {
|
|
2075
|
+
applyFlowTextLine(line, panel.lines[lineIndex], contentWidth);
|
|
2076
|
+
});
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// packages/cli/src/app-opentui/render/type-bar.ts
|
|
2081
|
+
import { InputRenderable, InputRenderableEvents, TextRenderable as TextRenderable2 } from "@opentui/core";
|
|
2082
|
+
function createTypeBar(renderer) {
|
|
2083
|
+
const prefix = new TextRenderable2(renderer, {
|
|
2084
|
+
id: "rig-typebar-prefix",
|
|
2085
|
+
content: " rig \u203A",
|
|
2086
|
+
position: "absolute",
|
|
2087
|
+
left: 0,
|
|
2088
|
+
top: 0,
|
|
2089
|
+
fg: RIG_UI.lime,
|
|
2090
|
+
selectable: true
|
|
2091
|
+
});
|
|
2092
|
+
const input = new InputRenderable(renderer, {
|
|
2093
|
+
id: "rig-typebar-input",
|
|
2094
|
+
position: "absolute",
|
|
2095
|
+
left: 7,
|
|
2096
|
+
top: 0,
|
|
2097
|
+
width: 40,
|
|
2098
|
+
placeholder: "fleet \xB7 tasks \xB7 run next \xB7 help",
|
|
2099
|
+
textColor: RIG_UI.ink,
|
|
2100
|
+
cursorColor: RIG_UI.lime,
|
|
2101
|
+
cursorStyle: { style: "block", blinking: false },
|
|
2102
|
+
showCursor: true,
|
|
2103
|
+
selectable: true
|
|
2104
|
+
});
|
|
2105
|
+
const footer = new TextRenderable2(renderer, {
|
|
2106
|
+
id: "rig-footer",
|
|
2107
|
+
content: "",
|
|
2108
|
+
position: "absolute",
|
|
2109
|
+
left: 0,
|
|
2110
|
+
top: 1,
|
|
2111
|
+
width: "100%",
|
|
2112
|
+
fg: RIG_UI.ink4,
|
|
2113
|
+
selectable: true
|
|
2114
|
+
});
|
|
2115
|
+
input.focus();
|
|
2116
|
+
return { input, prefix, footer };
|
|
2117
|
+
}
|
|
2118
|
+
function positionTypeBar(renderables, input, footer, width, typeBarTop, footerTop) {
|
|
2119
|
+
renderables.prefix.content = " rig \u203A";
|
|
2120
|
+
renderables.prefix.top = typeBarTop;
|
|
2121
|
+
renderables.input.top = typeBarTop;
|
|
2122
|
+
renderables.input.left = 7;
|
|
2123
|
+
renderables.input.width = Math.max(10, width - 9);
|
|
2124
|
+
renderables.input.placeholder = input.placeholder;
|
|
2125
|
+
renderables.input.showCursor = true;
|
|
2126
|
+
renderables.footer.top = footerTop;
|
|
2127
|
+
renderables.footer.width = "100%";
|
|
2128
|
+
renderables.footer.content = formatFooter(footer, width);
|
|
2129
|
+
if (!renderables.input.focused)
|
|
2130
|
+
renderables.input.focus();
|
|
2131
|
+
}
|
|
2132
|
+
function hideTypeBar(renderables, height) {
|
|
2133
|
+
renderables.prefix.content = "";
|
|
2134
|
+
renderables.prefix.top = height + 1;
|
|
2135
|
+
renderables.input.value = "";
|
|
2136
|
+
renderables.input.placeholder = "";
|
|
2137
|
+
renderables.input.top = height + 1;
|
|
2138
|
+
renderables.input.left = 0;
|
|
2139
|
+
renderables.input.width = 1;
|
|
2140
|
+
renderables.input.blur();
|
|
2141
|
+
renderables.footer.content = "";
|
|
2142
|
+
renderables.footer.top = height + 1;
|
|
2143
|
+
}
|
|
2144
|
+
function bindTypeBar(renderables, handlers) {
|
|
2145
|
+
renderables.input.on(InputRenderableEvents.INPUT, (value) => handlers.onInput(value));
|
|
2146
|
+
renderables.input.on(InputRenderableEvents.ENTER, (value) => handlers.onEnter(value));
|
|
2147
|
+
}
|
|
2148
|
+
function formatFooter(footer, width) {
|
|
2149
|
+
const parts = [
|
|
2150
|
+
footer.project ? `project ${footer.project}` : null,
|
|
2151
|
+
footer.server ? `server ${footer.server}` : null,
|
|
2152
|
+
footer.auth ? `auth ${footer.auth}` : null,
|
|
2153
|
+
footer.run ? `run ${footer.run}` : null,
|
|
2154
|
+
footer.message ?? null
|
|
2155
|
+
].filter((part) => Boolean(part));
|
|
2156
|
+
return ` ${truncateText(parts.join(" \xB7 "), Math.max(8, width - 2))}`;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
// packages/cli/src/app-opentui/render/scene.ts
|
|
2160
|
+
function line(text, options = {}) {
|
|
2161
|
+
return { text, ...options };
|
|
2162
|
+
}
|
|
2163
|
+
function blank() {
|
|
2164
|
+
return { text: "" };
|
|
2165
|
+
}
|
|
2166
|
+
function center(text, fg = RIG_UI.ink2, bold = false) {
|
|
2167
|
+
return { text, fg, bold, align: "center" };
|
|
2168
|
+
}
|
|
2169
|
+
function deckRow(input) {
|
|
2170
|
+
const prefix = input.active ? "\u25B8" : " ";
|
|
2171
|
+
const label = input.label.toUpperCase().padEnd(10);
|
|
2172
|
+
return line(`${prefix} ${label} ${input.detail}`, {
|
|
2173
|
+
fg: input.active ? RIG_UI.lime : RIG_UI.ink2,
|
|
2174
|
+
bold: input.active,
|
|
2175
|
+
selectableIndex: input.index,
|
|
2176
|
+
activateOnClick: input.activateOnClick
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
function fitSceneLine(input, layout) {
|
|
2180
|
+
const fitted = truncateText(input.text, layout.centerWidth);
|
|
2181
|
+
return {
|
|
2182
|
+
...input,
|
|
2183
|
+
text: alignText(fitted, layout.centerWidth, input.align ?? "left")
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
function fitSceneLines(lines, layout) {
|
|
2187
|
+
return lines.slice(0, layout.centerHeight).map((entry) => fitSceneLine(entry, layout));
|
|
2188
|
+
}
|
|
2189
|
+
function makeSceneFrame(input) {
|
|
2190
|
+
return input;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// packages/cli/src/app-opentui/drone.ts
|
|
2194
|
+
import { RGBA as RGBA3, StyledText, TextAttributes as TextAttributes3 } from "@opentui/core";
|
|
2195
|
+
var MINI_DRONE = [
|
|
2196
|
+
"(!!!) (!!!)",
|
|
2197
|
+
" \\%==%/ ",
|
|
2198
|
+
" %%?%% ",
|
|
2199
|
+
" /%==%\\ ",
|
|
2200
|
+
"(!!!) (!!!)"
|
|
2201
|
+
];
|
|
2202
|
+
var LEAD_MARK = [
|
|
2203
|
+
" .-=-. ",
|
|
2204
|
+
"=%%?%%=",
|
|
2205
|
+
" '-=-' "
|
|
2206
|
+
];
|
|
2207
|
+
var BLADE_FRAMES = ["---", "\\\\\\", "|||", "///"];
|
|
2208
|
+
var EYE_FRAMES = ["o", "@", "\u2022", "."];
|
|
2209
|
+
var COLOR = {
|
|
2210
|
+
body: RGBA3.fromHex(RIG_UI.lime),
|
|
2211
|
+
mini: RGBA3.fromHex(RIG_UI.limeDim),
|
|
2212
|
+
rotor: RGBA3.fromHex(RIG_UI.cyan),
|
|
2213
|
+
path: RGBA3.fromHex(RIG_UI.cyan),
|
|
2214
|
+
eye: RGBA3.fromHex(RIG_UI.ink),
|
|
2215
|
+
dim: RGBA3.fromHex(RIG_UI.ink4),
|
|
2216
|
+
ink: RGBA3.fromHex(RIG_UI.ink2)
|
|
2217
|
+
};
|
|
2218
|
+
function bladeForTick(tick, phase = 0) {
|
|
2219
|
+
return BLADE_FRAMES[(Math.floor(tick / 3) + phase) % BLADE_FRAMES.length];
|
|
2220
|
+
}
|
|
2221
|
+
function eyeForTick(tick, phase = 0) {
|
|
2222
|
+
const pulse = Math.sin(tick * 0.09 + phase);
|
|
2223
|
+
return pulse > 0.55 ? EYE_FRAMES[1] : pulse > 0.1 ? EYE_FRAMES[0] : pulse > -0.45 ? EYE_FRAMES[2] : EYE_FRAMES[3];
|
|
2224
|
+
}
|
|
2225
|
+
function chunk2(text, fg, bold = false, dim = false) {
|
|
2226
|
+
let attributes = TextAttributes3.NONE;
|
|
2227
|
+
if (bold)
|
|
2228
|
+
attributes |= TextAttributes3.BOLD;
|
|
2229
|
+
if (dim)
|
|
2230
|
+
attributes |= TextAttributes3.DIM;
|
|
2231
|
+
return { __isChunk: true, text, fg, ...attributes !== TextAttributes3.NONE ? { attributes } : {} };
|
|
2232
|
+
}
|
|
2233
|
+
function styledLine(text, colorFor) {
|
|
2234
|
+
const chunks = [];
|
|
2235
|
+
let run = "";
|
|
2236
|
+
let runColor = null;
|
|
2237
|
+
const flush = () => {
|
|
2238
|
+
if (!run || !runColor)
|
|
2239
|
+
return;
|
|
2240
|
+
chunks.push(chunk2(run, COLOR[runColor], runColor === "body" || runColor === "eye", runColor === "dim"));
|
|
2241
|
+
run = "";
|
|
2242
|
+
};
|
|
2243
|
+
for (const char of text) {
|
|
2244
|
+
const next = colorFor(char);
|
|
2245
|
+
if (next !== runColor) {
|
|
2246
|
+
flush();
|
|
2247
|
+
runColor = next;
|
|
2248
|
+
}
|
|
2249
|
+
run += char;
|
|
2250
|
+
}
|
|
2251
|
+
flush();
|
|
2252
|
+
return new StyledText(chunks.length > 0 ? chunks : [chunk2("", COLOR.ink)]);
|
|
2253
|
+
}
|
|
2254
|
+
function droneColor(char) {
|
|
2255
|
+
if (char === "?" || char === "o" || char === "@" || char === "\u2022")
|
|
2256
|
+
return "eye";
|
|
2257
|
+
if (char === "%" || char === "=")
|
|
2258
|
+
return "mini";
|
|
2259
|
+
if (char === "$")
|
|
2260
|
+
return "body";
|
|
2261
|
+
if (char === "(" || char === ")" || char === "/" || char === "\\" || char === "|" || char === "-")
|
|
2262
|
+
return "rotor";
|
|
2263
|
+
if (char === "\xB7" || char === "\u2022" || char === "'" || char === ".")
|
|
2264
|
+
return "path";
|
|
2265
|
+
if (char.trim() === "")
|
|
2266
|
+
return "dim";
|
|
2267
|
+
return "ink";
|
|
2268
|
+
}
|
|
2269
|
+
function motionColor(char) {
|
|
2270
|
+
if (char === "\u25CF" || char === "\u25C6" || char === "\u25C9")
|
|
2271
|
+
return "body";
|
|
2272
|
+
if (char === "\u2022" || char === "\xB7" || char === "\u2500" || char === "\u2502" || char === "\u2571" || char === "\u2572" || char === "\u256D" || char === "\u256E" || char === "\u2570" || char === "\u256F")
|
|
2273
|
+
return "path";
|
|
2274
|
+
if (char === "\u258C" || char === "\u2590" || char === "\u2581" || char === "\u2594")
|
|
2275
|
+
return "mini";
|
|
2276
|
+
return droneColor(char);
|
|
2277
|
+
}
|
|
2278
|
+
function materializeDrone(lines, tick, phase = 0) {
|
|
2279
|
+
const blade = bladeForTick(tick, phase);
|
|
2280
|
+
const eye = eyeForTick(tick, phase);
|
|
2281
|
+
return lines.map((line2) => line2.replaceAll("!!!", blade).replaceAll("?", eye));
|
|
2282
|
+
}
|
|
2283
|
+
function brandFleetLines(tick, align = "center") {
|
|
2284
|
+
const left = materializeDrone(MINI_DRONE, tick, 0);
|
|
2285
|
+
const right = materializeDrone(MINI_DRONE, tick, 2);
|
|
2286
|
+
const lead = materializeDrone(LEAD_MARK, tick, 1);
|
|
2287
|
+
const packet = ["\xB7", "\u2022", "\u25CF", "\u2022"][Math.floor(tick / 2) % 4];
|
|
2288
|
+
const bridge = [
|
|
2289
|
+
` ${left[0]} ${lead[0]} ${right[0]}`,
|
|
2290
|
+
` ${left[1]} \xB7\u2500\u2500${packet}\u2500\u2500\xB7 ${lead[1]} \xB7\u2500\u2500${packet}\u2500\u2500\xB7 ${right[1]}`,
|
|
2291
|
+
` ${left[2]} \xB7' '${lead[2]}' '\xB7 ${right[2]}`,
|
|
2292
|
+
` ${left[3]} \u2572 signal bus \u2571 ${right[3]}`,
|
|
2293
|
+
` ${left[4]} \xB7\u2500\u2500\u2500${packet}\u2500\u2500\u2500\u2500\u2500\u2500${packet}\u2500\u2500\u2500\xB7 ${right[4]}`
|
|
2294
|
+
];
|
|
2295
|
+
return bridge.map((text) => ({
|
|
2296
|
+
text,
|
|
2297
|
+
styledText: styledLine(text, motionColor),
|
|
2298
|
+
fg: RIG_UI.limeDim,
|
|
2299
|
+
align
|
|
2300
|
+
}));
|
|
2301
|
+
}
|
|
2302
|
+
function errorDrone(tick) {
|
|
2303
|
+
return brandFleetLines(tick).map((line2, index) => ({
|
|
2304
|
+
...line2,
|
|
2305
|
+
fg: index % 2 === 0 ? RIG_UI.red : line2.fg
|
|
2306
|
+
}));
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// packages/cli/src/app-opentui/scenes/error.ts
|
|
2310
|
+
function renderErrorScene(state) {
|
|
2311
|
+
const message = state.error?.message ?? "Unknown Rig app error";
|
|
2312
|
+
const hint = state.error?.hint ?? "type main, help, or retry after fixing the issue";
|
|
2313
|
+
return makeSceneFrame({
|
|
2314
|
+
scene: "error",
|
|
2315
|
+
title: "Rig error",
|
|
2316
|
+
lines: [
|
|
2317
|
+
...errorDrone(state.tick).slice(2, 16),
|
|
2318
|
+
blank(),
|
|
2319
|
+
center("DRONE SIGNAL DEGRADED", RIG_UI.red, true),
|
|
2320
|
+
center(message, RIG_UI.ink2),
|
|
2321
|
+
blank(),
|
|
2322
|
+
center(hint, RIG_UI.limeDim)
|
|
2323
|
+
],
|
|
2324
|
+
typeBarPlaceholder: hint,
|
|
2325
|
+
live: true
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// packages/cli/src/app-opentui/scenes/help.ts
|
|
2330
|
+
var COMMANDS = [
|
|
2331
|
+
["runs", "show runs"],
|
|
2332
|
+
["tasks", "show task source"],
|
|
2333
|
+
["type in Runs", "search loaded runs"],
|
|
2334
|
+
["type in Tasks", "search loaded tasks"],
|
|
2335
|
+
["tasks open", "filter to open tasks"],
|
|
2336
|
+
["tasks all", "show every task returned by the source"],
|
|
2337
|
+
["tasks mine", "show tasks assigned to @me"],
|
|
2338
|
+
["tasks blocked", "show blocked work"],
|
|
2339
|
+
["tasks closed", "show closed work"],
|
|
2340
|
+
["tasks sort priority", "rank by priority"],
|
|
2341
|
+
["tasks sort recency", "rank by update time"],
|
|
2342
|
+
["tasks sort status", "group by status"],
|
|
2343
|
+
["run <task-id>", "dispatch selected task id"],
|
|
2344
|
+
["run next", "dispatch next runnable task"],
|
|
2345
|
+
["attach <run>", "open bundled Pi for a run"],
|
|
2346
|
+
["tab / \u2192", "next screen"],
|
|
2347
|
+
["\u2190", "previous screen"],
|
|
2348
|
+
["\u2191 \u2193", "move selection in Runs/Tasks"],
|
|
2349
|
+
["j k", "move selection outside search-first list screens"],
|
|
2350
|
+
["enter", "activate selected row"],
|
|
2351
|
+
["mouse wheel", "scroll panel under cursor"],
|
|
2352
|
+
["ctrl-]", "detach from embedded Pi"],
|
|
2353
|
+
["ctrl-c / q", "exit"]
|
|
2354
|
+
];
|
|
2355
|
+
function panelWidth(layout) {
|
|
2356
|
+
return layout?.centerWidth ?? 100;
|
|
2357
|
+
}
|
|
2358
|
+
function panelHeight(layout, top = 3) {
|
|
2359
|
+
return Math.max(4, (layout?.centerHeight ?? 24) - top);
|
|
2360
|
+
}
|
|
2361
|
+
function clip(value, width) {
|
|
2362
|
+
if (width <= 0)
|
|
2363
|
+
return "";
|
|
2364
|
+
return value.length <= width ? value : `${value.slice(0, Math.max(0, width - 1))}\u2026`;
|
|
2365
|
+
}
|
|
2366
|
+
function commandRow(width, label, detail, index) {
|
|
2367
|
+
const keyWidth = Math.min(22, Math.max(12, Math.floor(width * 0.28)));
|
|
2368
|
+
const key = clip(label, keyWidth).padEnd(keyWidth);
|
|
2369
|
+
return line(`${key} ${clip(detail, Math.max(8, width - keyWidth - 4))}`, { fg: index < 3 ? RIG_UI.ink : RIG_UI.ink2 });
|
|
2370
|
+
}
|
|
2371
|
+
function unsupportedCommand(state) {
|
|
2372
|
+
const value = state.intent.action.payload?.unsupported ?? state.intent.action.payload?.unknown;
|
|
2373
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
2374
|
+
}
|
|
2375
|
+
function renderHelpScene(state, layout) {
|
|
2376
|
+
const width = panelWidth(layout);
|
|
2377
|
+
const unsupported = unsupportedCommand(state);
|
|
2378
|
+
const panelTop = 0;
|
|
2379
|
+
const panelLines = [
|
|
2380
|
+
...unsupported ? [
|
|
2381
|
+
line(`${unsupported} uses the plain CLI path.`, { fg: RIG_UI.yellow, bold: true }),
|
|
2382
|
+
line("Exit and rerun with RIG_PLAIN=1 if you want legacy text output.", { fg: RIG_UI.ink3 }),
|
|
2383
|
+
line("", { fg: RIG_UI.ink3 })
|
|
2384
|
+
] : [],
|
|
2385
|
+
line("COMMAND ACTION", { fg: RIG_UI.ink3, bold: true }),
|
|
2386
|
+
line("", { fg: RIG_UI.ink3 }),
|
|
2387
|
+
...COMMANDS.map(([label, detail], index) => commandRow(width - 6, label, detail, index))
|
|
2388
|
+
];
|
|
2389
|
+
return makeSceneFrame({
|
|
2390
|
+
scene: "help",
|
|
2391
|
+
title: "Rig help",
|
|
2392
|
+
lines: [
|
|
2393
|
+
line("", { fg: RIG_UI.ink4 }),
|
|
2394
|
+
line(" rig help", { fg: RIG_UI.ink3 })
|
|
2395
|
+
],
|
|
2396
|
+
panels: [{
|
|
2397
|
+
id: "help-reference",
|
|
2398
|
+
top: panelTop,
|
|
2399
|
+
width,
|
|
2400
|
+
height: panelHeight(layout, panelTop),
|
|
2401
|
+
lines: panelLines,
|
|
2402
|
+
backgroundColor: RIG_UI.panel,
|
|
2403
|
+
backgroundAlpha: 184,
|
|
2404
|
+
opacity: 1,
|
|
2405
|
+
border: false,
|
|
2406
|
+
chrome: "ad-terminal",
|
|
2407
|
+
headerText: "rig help",
|
|
2408
|
+
headerHeight: 3,
|
|
2409
|
+
paddingX: layout?.compact ? 2 : 3,
|
|
2410
|
+
paddingY: 1
|
|
2411
|
+
}],
|
|
2412
|
+
typeBarPlaceholder: "runs \xB7 tasks \xB7 help \xB7 attach <run>",
|
|
2413
|
+
footer: { message: "help" },
|
|
2414
|
+
live: true
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// packages/cli/src/app-opentui/scenes/main.ts
|
|
2419
|
+
var CONFIGURED_OPTIONS = [
|
|
2420
|
+
["runs", "run list and attach"],
|
|
2421
|
+
["tasks", "task source and dispatch"],
|
|
2422
|
+
["help", "command reference"]
|
|
2423
|
+
];
|
|
2424
|
+
function renderMainScene(state) {
|
|
2425
|
+
const configured = state.data.projectConfigured === true;
|
|
2426
|
+
const selected = configured ? Math.max(0, Math.min(CONFIGURED_OPTIONS.length - 1, state.selection.index)) : Math.max(0, Math.min(1, state.selection.index));
|
|
2427
|
+
const lines = [
|
|
2428
|
+
center(configured ? "rig" : "rig setup", RIG_UI.ink, true),
|
|
2429
|
+
center(configured ? "runs \xB7 tasks \xB7 help" : "initialize this repo", RIG_UI.ink3),
|
|
2430
|
+
blank()
|
|
2431
|
+
];
|
|
2432
|
+
if (!configured) {
|
|
2433
|
+
lines.push(deckRow({ label: "init", detail: "create config and server link", index: 0, active: selected === 0 }), deckRow({ label: "help", detail: "show command reference", index: 1, active: selected === 1 }), blank(), line("enter opens selection", { fg: RIG_UI.ink4, align: "center" }));
|
|
2434
|
+
} else {
|
|
2435
|
+
CONFIGURED_OPTIONS.forEach(([label, detail], index) => {
|
|
2436
|
+
lines.push(deckRow({ label, detail, index, active: index === selected }));
|
|
2437
|
+
});
|
|
2438
|
+
lines.push(blank(), line(` ${state.actionLabel ?? "ready"}`, { fg: state.status === "action" ? RIG_UI.cyan : RIG_UI.ink4 }));
|
|
2439
|
+
}
|
|
2440
|
+
return makeSceneFrame({
|
|
2441
|
+
scene: "main",
|
|
2442
|
+
title: "Rig",
|
|
2443
|
+
lines,
|
|
2444
|
+
typeBarPlaceholder: configured ? "runs \xB7 tasks \xB7 help" : "init \xB7 help",
|
|
2445
|
+
live: true
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// packages/cli/src/app-opentui/pi-pty-host.ts
|
|
2450
|
+
import { fileURLToPath } from "url";
|
|
2451
|
+
import { basename } from "path";
|
|
2452
|
+
import { RGBA as RGBA4, StyledText as StyledText2, TextAttributes as TextAttributes4 } from "@opentui/core";
|
|
2453
|
+
import { Terminal as XtermTerminal } from "@xterm/headless";
|
|
2454
|
+
var MIN_COLS = 40;
|
|
2455
|
+
var MIN_ROWS = 12;
|
|
2456
|
+
var MAX_ROWS = 300;
|
|
2457
|
+
var MAX_SNAPSHOT_LINES = 1200;
|
|
2458
|
+
var fallbackChildScriptPath = fileURLToPath(new URL("./pi-host-child.ts", import.meta.url));
|
|
2459
|
+
var activeHost = null;
|
|
2460
|
+
function clampCols(cols) {
|
|
2461
|
+
return Math.max(MIN_COLS, Math.trunc(cols || 100));
|
|
2462
|
+
}
|
|
2463
|
+
function clampRows(rows) {
|
|
2464
|
+
return Math.max(MIN_ROWS, Math.min(MAX_ROWS, Math.trunc(rows || 35)));
|
|
2465
|
+
}
|
|
2466
|
+
var XTERM_COLOR_MODE_INDEXED_16 = 16777216;
|
|
2467
|
+
var XTERM_COLOR_MODE_INDEXED_256 = 33554432;
|
|
2468
|
+
var XTERM_COLOR_MODE_RGB = 50331648;
|
|
2469
|
+
function rgbaFromXtermColor(mode, value) {
|
|
2470
|
+
if (mode === 1 || mode === 2 || mode === XTERM_COLOR_MODE_INDEXED_16 || mode === XTERM_COLOR_MODE_INDEXED_256) {
|
|
2471
|
+
return RGBA4.fromIndex(value);
|
|
2472
|
+
}
|
|
2473
|
+
if (mode === 3 || mode === XTERM_COLOR_MODE_RGB) {
|
|
2474
|
+
return RGBA4.fromInts(value >> 16 & 255, value >> 8 & 255, value & 255);
|
|
2475
|
+
}
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
function textAttributesFromCell(cell) {
|
|
2479
|
+
let attributes = TextAttributes4.NONE;
|
|
2480
|
+
if (cell.isBold())
|
|
2481
|
+
attributes |= TextAttributes4.BOLD;
|
|
2482
|
+
if (cell.isDim())
|
|
2483
|
+
attributes |= TextAttributes4.DIM;
|
|
2484
|
+
if (cell.isItalic())
|
|
2485
|
+
attributes |= TextAttributes4.ITALIC;
|
|
2486
|
+
if (cell.isUnderline())
|
|
2487
|
+
attributes |= TextAttributes4.UNDERLINE;
|
|
2488
|
+
if (cell.isBlink())
|
|
2489
|
+
attributes |= TextAttributes4.BLINK;
|
|
2490
|
+
if (cell.isInverse())
|
|
2491
|
+
attributes |= TextAttributes4.INVERSE;
|
|
2492
|
+
if (cell.isInvisible())
|
|
2493
|
+
attributes |= TextAttributes4.HIDDEN;
|
|
2494
|
+
if (cell.isStrikethrough())
|
|
2495
|
+
attributes |= TextAttributes4.STRIKETHROUGH;
|
|
2496
|
+
return attributes;
|
|
2497
|
+
}
|
|
2498
|
+
function sameRgba(a, b) {
|
|
2499
|
+
if (!a && !b)
|
|
2500
|
+
return true;
|
|
2501
|
+
return Boolean(a && b && a.equals(b));
|
|
2502
|
+
}
|
|
2503
|
+
function sameStyle(a, b) {
|
|
2504
|
+
return sameRgba(a.fg, b.fg) && sameRgba(a.bg, b.bg) && (a.attributes ?? 0) === (b.attributes ?? 0);
|
|
2505
|
+
}
|
|
2506
|
+
function styleFromCell(cell) {
|
|
2507
|
+
return {
|
|
2508
|
+
fg: rgbaFromXtermColor(cell.getFgColorMode(), cell.getFgColor()),
|
|
2509
|
+
bg: rgbaFromXtermColor(cell.getBgColorMode(), cell.getBgColor()),
|
|
2510
|
+
attributes: textAttributesFromCell(cell)
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
function lineToStyledText(line2, cols) {
|
|
2514
|
+
if (!line2)
|
|
2515
|
+
return new StyledText2([{ __isChunk: true, text: "" }]);
|
|
2516
|
+
const chunks = [];
|
|
2517
|
+
let run = "";
|
|
2518
|
+
let runStyle = null;
|
|
2519
|
+
const flush = () => {
|
|
2520
|
+
if (!run)
|
|
2521
|
+
return;
|
|
2522
|
+
chunks.push({
|
|
2523
|
+
__isChunk: true,
|
|
2524
|
+
text: run,
|
|
2525
|
+
...runStyle?.fg ? { fg: runStyle.fg } : {},
|
|
2526
|
+
...runStyle?.bg ? { bg: runStyle.bg } : {},
|
|
2527
|
+
...(runStyle?.attributes ?? 0) !== 0 ? { attributes: runStyle.attributes } : {}
|
|
2528
|
+
});
|
|
2529
|
+
run = "";
|
|
2530
|
+
};
|
|
2531
|
+
for (let index = 0;index < cols; index += 1) {
|
|
2532
|
+
const cell = line2.getCell(index);
|
|
2533
|
+
if (!cell) {
|
|
2534
|
+
if (runStyle !== null)
|
|
2535
|
+
flush();
|
|
2536
|
+
runStyle = null;
|
|
2537
|
+
run += " ";
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
if (cell.getWidth() === 0)
|
|
2541
|
+
continue;
|
|
2542
|
+
const style = styleFromCell(cell);
|
|
2543
|
+
if (!runStyle || !sameStyle(runStyle, style)) {
|
|
2544
|
+
flush();
|
|
2545
|
+
runStyle = style;
|
|
2546
|
+
}
|
|
2547
|
+
run += cell.getChars() || " ";
|
|
2548
|
+
}
|
|
2549
|
+
flush();
|
|
2550
|
+
return new StyledText2(chunks.length > 0 ? chunks : [{ __isChunk: true, text: "" }]);
|
|
2551
|
+
}
|
|
2552
|
+
function childCommandPrefix() {
|
|
2553
|
+
const execName = basename(process.execPath).toLowerCase();
|
|
2554
|
+
const currentEntry = process.argv[1];
|
|
2555
|
+
if ((execName === "bun" || execName === "bun.exe") && currentEntry) {
|
|
2556
|
+
return [process.execPath, currentEntry, "__opentui-pi-host"];
|
|
2557
|
+
}
|
|
2558
|
+
if (currentEntry)
|
|
2559
|
+
return [process.execPath, "__opentui-pi-host"];
|
|
2560
|
+
return [process.execPath, fallbackChildScriptPath];
|
|
2561
|
+
}
|
|
2562
|
+
function withEnv(base) {
|
|
2563
|
+
const env = {};
|
|
2564
|
+
for (const [key, value] of Object.entries(base ?? process.env)) {
|
|
2565
|
+
if (key === "RIG_PLAIN" || key === "RIG_CLI_PLAIN_HELP")
|
|
2566
|
+
continue;
|
|
2567
|
+
if (typeof value === "string")
|
|
2568
|
+
env[key] = value;
|
|
2569
|
+
}
|
|
2570
|
+
return {
|
|
2571
|
+
...env,
|
|
2572
|
+
TERM: "xterm-256color",
|
|
2573
|
+
COLORTERM: "truecolor",
|
|
2574
|
+
FORCE_COLOR: env.FORCE_COLOR ?? "1",
|
|
2575
|
+
RIG_OPENTUI_PI_HOST: "1"
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
function getActivePiHost() {
|
|
2579
|
+
return activeHost && !activeHost.disposed ? activeHost : null;
|
|
2580
|
+
}
|
|
2581
|
+
function stopActivePiHost(reason = "detach") {
|
|
2582
|
+
activeHost?.dispose(reason);
|
|
2583
|
+
activeHost = null;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
class PiPtyHost {
|
|
2587
|
+
runId;
|
|
2588
|
+
projectRoot;
|
|
2589
|
+
onSnapshot;
|
|
2590
|
+
onExit;
|
|
2591
|
+
onError;
|
|
2592
|
+
terminal;
|
|
2593
|
+
disposables = [];
|
|
2594
|
+
decoder = new TextDecoder("utf-8");
|
|
2595
|
+
proc = null;
|
|
2596
|
+
pty = null;
|
|
2597
|
+
status = "starting";
|
|
2598
|
+
cols;
|
|
2599
|
+
rows;
|
|
2600
|
+
message = "starting bundled Pi";
|
|
2601
|
+
exitCode;
|
|
2602
|
+
signal;
|
|
2603
|
+
notifyTimer = null;
|
|
2604
|
+
_disposed = false;
|
|
2605
|
+
constructor(options) {
|
|
2606
|
+
this.runId = options.runId;
|
|
2607
|
+
this.projectRoot = options.projectRoot;
|
|
2608
|
+
this.cols = clampCols(options.cols);
|
|
2609
|
+
this.rows = clampRows(options.rows);
|
|
2610
|
+
this.onSnapshot = options.onSnapshot;
|
|
2611
|
+
this.onExit = options.onExit;
|
|
2612
|
+
this.onError = options.onError;
|
|
2613
|
+
this.terminal = new XtermTerminal({
|
|
2614
|
+
allowProposedApi: true,
|
|
2615
|
+
cols: this.cols,
|
|
2616
|
+
rows: this.rows,
|
|
2617
|
+
scrollback: 5000
|
|
2618
|
+
});
|
|
2619
|
+
this.registerTerminalResponders();
|
|
2620
|
+
}
|
|
2621
|
+
get disposed() {
|
|
2622
|
+
return this._disposed;
|
|
2623
|
+
}
|
|
2624
|
+
get snapshot() {
|
|
2625
|
+
return this.createSnapshot();
|
|
2626
|
+
}
|
|
2627
|
+
async start() {
|
|
2628
|
+
if (this._disposed)
|
|
2629
|
+
throw new Error("Pi PTY host is disposed.");
|
|
2630
|
+
if (typeof Bun.Terminal !== "function") {
|
|
2631
|
+
throw new Error("Bun native PTY is unavailable in this runtime. Rig OpenTUI Pi host requires Bun.Terminal.");
|
|
2632
|
+
}
|
|
2633
|
+
const spawnOptions = {
|
|
2634
|
+
cwd: this.projectRoot,
|
|
2635
|
+
env: withEnv(process.env),
|
|
2636
|
+
terminal: {
|
|
2637
|
+
cols: this.cols,
|
|
2638
|
+
rows: this.rows,
|
|
2639
|
+
name: "xterm-256color",
|
|
2640
|
+
data: (_terminal, data) => this.handlePtyData(data)
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
const proc = Bun.spawn([
|
|
2644
|
+
...childCommandPrefix(),
|
|
2645
|
+
"--run-id",
|
|
2646
|
+
this.runId,
|
|
2647
|
+
"--project-root",
|
|
2648
|
+
this.projectRoot
|
|
2649
|
+
], spawnOptions);
|
|
2650
|
+
if (!proc.terminal)
|
|
2651
|
+
throw new Error("Bun did not attach a terminal to the bundled Pi child process.");
|
|
2652
|
+
this.proc = proc;
|
|
2653
|
+
this.pty = proc.terminal;
|
|
2654
|
+
this.status = "running";
|
|
2655
|
+
this.message = "bundled Pi running inside Rig";
|
|
2656
|
+
this.emitSnapshotSoon(0);
|
|
2657
|
+
proc.exited.then((exitCode) => {
|
|
2658
|
+
if (this._disposed)
|
|
2659
|
+
return;
|
|
2660
|
+
this.status = exitCode === 0 ? "exited" : "failed";
|
|
2661
|
+
this.exitCode = exitCode;
|
|
2662
|
+
this.signal = null;
|
|
2663
|
+
this.message = exitCode === 0 ? "bundled Pi exited" : `bundled Pi exited with code ${exitCode}`;
|
|
2664
|
+
const snapshot = this.createSnapshot();
|
|
2665
|
+
this.onSnapshot?.(snapshot);
|
|
2666
|
+
this.onExit?.(snapshot);
|
|
2667
|
+
if (activeHost === this)
|
|
2668
|
+
activeHost = null;
|
|
2669
|
+
this.dispose("exit", { kill: false, notify: false });
|
|
2670
|
+
}).catch((error) => {
|
|
2671
|
+
if (this._disposed)
|
|
2672
|
+
return;
|
|
2673
|
+
this.status = "failed";
|
|
2674
|
+
this.message = error instanceof Error ? error.message : String(error);
|
|
2675
|
+
const snapshot = this.createSnapshot();
|
|
2676
|
+
this.onSnapshot?.(snapshot);
|
|
2677
|
+
this.onError?.(error, snapshot);
|
|
2678
|
+
if (activeHost === this)
|
|
2679
|
+
activeHost = null;
|
|
2680
|
+
this.dispose("error", { kill: false, notify: false });
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
write(data) {
|
|
2684
|
+
if (this._disposed || !this.pty)
|
|
2685
|
+
return;
|
|
2686
|
+
try {
|
|
2687
|
+
this.pty.write(data);
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
this.status = "failed";
|
|
2690
|
+
this.message = error instanceof Error ? error.message : String(error);
|
|
2691
|
+
const snapshot = this.createSnapshot();
|
|
2692
|
+
this.onSnapshot?.(snapshot);
|
|
2693
|
+
this.onError?.(error, snapshot);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
resize(cols, rows) {
|
|
2697
|
+
const nextCols = clampCols(cols);
|
|
2698
|
+
const nextRows = clampRows(rows);
|
|
2699
|
+
if (nextCols === this.cols && nextRows === this.rows)
|
|
2700
|
+
return;
|
|
2701
|
+
this.cols = nextCols;
|
|
2702
|
+
this.rows = nextRows;
|
|
2703
|
+
this.terminal.resize(nextCols, nextRows);
|
|
2704
|
+
try {
|
|
2705
|
+
this.pty?.resize(nextCols, nextRows);
|
|
2706
|
+
} catch {}
|
|
2707
|
+
this.emitSnapshotSoon(0);
|
|
2708
|
+
}
|
|
2709
|
+
detach() {
|
|
2710
|
+
this.dispose("detach");
|
|
2711
|
+
return this.createSnapshot("detached from bundled Pi");
|
|
2712
|
+
}
|
|
2713
|
+
dispose(reason = "dispose", options = {}) {
|
|
2714
|
+
if (this._disposed)
|
|
2715
|
+
return;
|
|
2716
|
+
this._disposed = true;
|
|
2717
|
+
if (this.notifyTimer)
|
|
2718
|
+
clearTimeout(this.notifyTimer);
|
|
2719
|
+
this.notifyTimer = null;
|
|
2720
|
+
for (const disposable of this.disposables.splice(0)) {
|
|
2721
|
+
try {
|
|
2722
|
+
disposable.dispose();
|
|
2723
|
+
} catch {}
|
|
2724
|
+
}
|
|
2725
|
+
if (options.kill !== false) {
|
|
2726
|
+
try {
|
|
2727
|
+
this.proc?.kill("SIGTERM");
|
|
2728
|
+
} catch {}
|
|
2729
|
+
}
|
|
2730
|
+
try {
|
|
2731
|
+
this.pty?.close();
|
|
2732
|
+
} catch {}
|
|
2733
|
+
try {
|
|
2734
|
+
this.terminal.dispose();
|
|
2735
|
+
} catch {}
|
|
2736
|
+
if (activeHost === this)
|
|
2737
|
+
activeHost = null;
|
|
2738
|
+
if (options.notify) {
|
|
2739
|
+
this.message = reason;
|
|
2740
|
+
this.onSnapshot?.(this.createSnapshot(reason));
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
handlePtyData(data) {
|
|
2744
|
+
if (this._disposed)
|
|
2745
|
+
return;
|
|
2746
|
+
const text = this.decoder.decode(data, { stream: true });
|
|
2747
|
+
this.respondToRawTerminalQueries(text);
|
|
2748
|
+
this.terminal.write(data, () => this.emitSnapshotSoon());
|
|
2749
|
+
}
|
|
2750
|
+
emitSnapshotSoon(delayMs = 16) {
|
|
2751
|
+
if (this._disposed || this.notifyTimer)
|
|
2752
|
+
return;
|
|
2753
|
+
this.notifyTimer = setTimeout(() => {
|
|
2754
|
+
this.notifyTimer = null;
|
|
2755
|
+
if (this._disposed)
|
|
2756
|
+
return;
|
|
2757
|
+
this.onSnapshot?.(this.createSnapshot());
|
|
2758
|
+
}, delayMs);
|
|
2759
|
+
}
|
|
2760
|
+
createSnapshot(message = this.message) {
|
|
2761
|
+
const buffer = this.terminal.buffer.active;
|
|
2762
|
+
const end = buffer.length;
|
|
2763
|
+
const start = Math.max(0, end - MAX_SNAPSHOT_LINES);
|
|
2764
|
+
const lines = [];
|
|
2765
|
+
const styledLines = [];
|
|
2766
|
+
for (let row = start;row < end; row += 1) {
|
|
2767
|
+
const line2 = buffer.getLine(row);
|
|
2768
|
+
lines.push((line2?.translateToString(false) ?? "").slice(0, this.cols));
|
|
2769
|
+
styledLines.push(lineToStyledText(line2, this.cols));
|
|
2770
|
+
}
|
|
2771
|
+
while (lines.length < this.rows) {
|
|
2772
|
+
lines.push("");
|
|
2773
|
+
styledLines.push(new StyledText2([{ __isChunk: true, text: "" }]));
|
|
2774
|
+
}
|
|
2775
|
+
return {
|
|
2776
|
+
runId: this.runId,
|
|
2777
|
+
status: this.status,
|
|
2778
|
+
cols: this.cols,
|
|
2779
|
+
rows: this.rows,
|
|
2780
|
+
lines,
|
|
2781
|
+
styledLines,
|
|
2782
|
+
message,
|
|
2783
|
+
...this.exitCode !== undefined ? { exitCode: this.exitCode } : {},
|
|
2784
|
+
...this.signal !== undefined ? { signal: this.signal } : {}
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
registerTerminalResponders() {
|
|
2788
|
+
this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "c" }, (params) => {
|
|
2789
|
+
if (params.length === 0 || params[0] === 0)
|
|
2790
|
+
this.write("\x1B[?62;22c");
|
|
2791
|
+
return false;
|
|
2792
|
+
}));
|
|
2793
|
+
this.disposables.push(this.terminal.parser.registerCsiHandler({ prefix: ">", final: "c" }, (params) => {
|
|
2794
|
+
if (params.length === 0 || params[0] === 0)
|
|
2795
|
+
this.write("\x1B[>0;0;0c");
|
|
2796
|
+
return false;
|
|
2797
|
+
}));
|
|
2798
|
+
this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "n" }, (params) => {
|
|
2799
|
+
if (params[0] === 5) {
|
|
2800
|
+
this.write("\x1B[0n");
|
|
2801
|
+
} else if (params[0] === 6) {
|
|
2802
|
+
const row = this.terminal.buffer.active.cursorY + 1;
|
|
2803
|
+
const col = this.terminal.buffer.active.cursorX + 1;
|
|
2804
|
+
this.write(`\x1B[${row};${col}R`);
|
|
2805
|
+
}
|
|
2806
|
+
return false;
|
|
2807
|
+
}));
|
|
2808
|
+
}
|
|
2809
|
+
respondToRawTerminalQueries(text) {
|
|
2810
|
+
if (!text)
|
|
2811
|
+
return;
|
|
2812
|
+
const decrqm = /\x1b\[\?(\d+)\$p/g;
|
|
2813
|
+
let match;
|
|
2814
|
+
while ((match = decrqm.exec(text)) !== null) {
|
|
2815
|
+
this.write(`\x1B[?${match[1]};2$y`);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
// packages/cli/src/app-opentui/runtime.ts
|
|
2821
|
+
var MAX_SCENE_LINES = 200;
|
|
2822
|
+
var MAX_SCENE_PANELS = 5;
|
|
2823
|
+
var FRAME_MS = 16;
|
|
2824
|
+
var ANIMATION_SLOWDOWN = 3;
|
|
2825
|
+
var PRIMARY_NAV = [
|
|
2826
|
+
{ scene: "fleet", argv: ["runs"], label: "Opening runs" },
|
|
2827
|
+
{ scene: "tasks", argv: ["tasks"], label: "Opening tasks" },
|
|
2828
|
+
{ scene: "help", argv: ["help"], label: "Opening help" }
|
|
2829
|
+
];
|
|
2830
|
+
function primaryNavIntent(scene) {
|
|
2831
|
+
const entry = PRIMARY_NAV.find((item) => item.scene === scene) ?? PRIMARY_NAV[0];
|
|
2832
|
+
return { scene: entry.scene, argv: entry.argv, action: { kind: entry.scene === "help" ? "none" : "refresh", label: entry.label } };
|
|
2833
|
+
}
|
|
2834
|
+
function nextPrimaryScene(current, delta) {
|
|
2835
|
+
const currentIndex = PRIMARY_NAV.findIndex((entry) => entry.scene === current);
|
|
2836
|
+
const index = currentIndex < 0 ? 0 : currentIndex;
|
|
2837
|
+
return PRIMARY_NAV[(index + delta + PRIMARY_NAV.length) % PRIMARY_NAV.length].scene;
|
|
2838
|
+
}
|
|
2839
|
+
function detectProjectConfigured(projectRoot) {
|
|
2840
|
+
return [
|
|
2841
|
+
"rig.config.ts",
|
|
2842
|
+
"rig.config.json",
|
|
2843
|
+
".rig/state/connection.json",
|
|
2844
|
+
"rig/task-config.json"
|
|
2845
|
+
].some((relative) => existsSync(resolve(projectRoot, relative)));
|
|
2846
|
+
}
|
|
2847
|
+
function defaultSceneRenderer(state) {
|
|
2848
|
+
const title = state.scene;
|
|
2849
|
+
return {
|
|
2850
|
+
scene: state.scene,
|
|
2851
|
+
title,
|
|
2852
|
+
lines: [
|
|
2853
|
+
{ text: title.toUpperCase(), fg: RIG_UI.ink, bold: true, align: "center" },
|
|
2854
|
+
{ text: state.actionLabel ?? "adapter scene pending", fg: RIG_UI.limeDim, align: "center" },
|
|
2855
|
+
{ text: "support state", fg: RIG_UI.ink3, align: "center" },
|
|
2856
|
+
{ text: "runs \xB7 tasks \xB7 help", fg: RIG_UI.ink4, align: "center" }
|
|
2857
|
+
],
|
|
2858
|
+
typeBarPlaceholder: "runs \xB7 tasks \xB7 help",
|
|
2859
|
+
live: false
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
function rendererForScene(scene, extra) {
|
|
2863
|
+
if (extra?.[scene])
|
|
2864
|
+
return extra[scene];
|
|
2865
|
+
switch (scene) {
|
|
2866
|
+
case "main":
|
|
2867
|
+
return renderMainScene;
|
|
2868
|
+
case "help":
|
|
2869
|
+
return renderHelpScene;
|
|
2870
|
+
case "error":
|
|
2871
|
+
return renderErrorScene;
|
|
2872
|
+
default:
|
|
2873
|
+
return defaultSceneRenderer;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
function requestRender(renderer) {
|
|
2877
|
+
renderer.requestRender?.();
|
|
2878
|
+
}
|
|
2879
|
+
function recordArray(value) {
|
|
2880
|
+
return Array.isArray(value) ? value.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
2881
|
+
}
|
|
2882
|
+
function stringField(record, keys) {
|
|
2883
|
+
for (const key of keys) {
|
|
2884
|
+
const value = record[key];
|
|
2885
|
+
if (typeof value === "string" && value.trim())
|
|
2886
|
+
return value.trim();
|
|
2887
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
2888
|
+
return String(value);
|
|
2889
|
+
}
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
function mainSelectableItems(_state) {
|
|
2893
|
+
return [
|
|
2894
|
+
{ id: "runs", label: "runs", intent: { scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } }, message: "selected runs" },
|
|
2895
|
+
{ id: "tasks", label: "tasks", intent: { scene: "tasks", argv: ["tasks"], action: { kind: "refresh", label: "Opening tasks" } }, message: "selected tasks" },
|
|
2896
|
+
{ id: "help", label: "help", intent: { scene: "help", argv: ["help"], action: { kind: "none", label: "Opening help" } }, message: "selected help" }
|
|
2897
|
+
];
|
|
2898
|
+
}
|
|
2899
|
+
function fleetSelectableItems(state) {
|
|
2900
|
+
const fleet = state.data.fleet;
|
|
2901
|
+
const runs = fleet && typeof fleet === "object" && !Array.isArray(fleet) ? recordArray(fleet.runs) : [];
|
|
2902
|
+
return filterRunsForSearch(runs, listSearchQuery(state)).flatMap((run) => {
|
|
2903
|
+
const runId = stringField(run, ["runId", "id"]);
|
|
2904
|
+
if (!runId)
|
|
2905
|
+
return [];
|
|
2906
|
+
const title = stringField(run, ["title", "taskTitle", "taskId"]) ?? "run";
|
|
2907
|
+
return [{ id: runId, label: title, data: { selectedRunId: runId }, intent: { scene: "handoff", argv: ["attach", runId], action: { kind: "run-attach", payload: { runId }, label: `Attach Pi ${runId.slice(0, 8)}` } }, message: `attach Pi to ${runId.slice(0, 8)}` }];
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
function taskSelectableItems(state) {
|
|
2911
|
+
const tasks = state.data.tasks;
|
|
2912
|
+
const records = tasks && typeof tasks === "object" && !Array.isArray(tasks) ? recordArray(tasks.records) : [];
|
|
2913
|
+
return filterTasksForSearch(records, listSearchQuery(state)).flatMap((task) => {
|
|
2914
|
+
const taskId = stringField(task, ["id", "taskId", "number"]);
|
|
2915
|
+
if (!taskId)
|
|
2916
|
+
return [];
|
|
2917
|
+
const title = stringField(task, ["title", "name", "summary"]) ?? "task";
|
|
2918
|
+
return [{ id: taskId, label: title, data: { selectedTaskId: taskId }, intent: { scene: "tasks", argv: ["run", taskId], action: { kind: "task-run-id", payload: { task: taskId }, label: `Dispatching ${taskId}` } }, message: `dispatch task ${taskId}` }];
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
function selectedRunId(state) {
|
|
2922
|
+
const selected = state.data.selectedRunId;
|
|
2923
|
+
if (typeof selected === "string" && selected.trim())
|
|
2924
|
+
return selected.trim();
|
|
2925
|
+
const detail = state.data.runDetail;
|
|
2926
|
+
if (detail && typeof detail === "object" && !Array.isArray(detail)) {
|
|
2927
|
+
const runId = detail.runId;
|
|
2928
|
+
if (typeof runId === "string" && runId.trim())
|
|
2929
|
+
return runId.trim();
|
|
2930
|
+
}
|
|
2931
|
+
const latest = state.data.latestSubmittedRun;
|
|
2932
|
+
if (latest && typeof latest === "object" && !Array.isArray(latest)) {
|
|
2933
|
+
const runId = latest.runId;
|
|
2934
|
+
if (typeof runId === "string" && runId.trim())
|
|
2935
|
+
return runId.trim();
|
|
2936
|
+
}
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
function inboxSelectableItems(state) {
|
|
2940
|
+
const inbox = state.data.inbox;
|
|
2941
|
+
if (!inbox || typeof inbox !== "object" || Array.isArray(inbox))
|
|
2942
|
+
return [];
|
|
2943
|
+
const record = inbox;
|
|
2944
|
+
const approvals = recordArray(record.approvals).map((entry) => ({ ...entry, kind: "approvals" }));
|
|
2945
|
+
const inputs = recordArray(record.inputs).map((entry) => ({ ...entry, kind: "inputs" }));
|
|
2946
|
+
return [...approvals, ...inputs].flatMap((item) => {
|
|
2947
|
+
const requestId = stringField(item, ["requestId", "id"]);
|
|
2948
|
+
if (!requestId)
|
|
2949
|
+
return [];
|
|
2950
|
+
const kind = item.kind === "inputs" ? "answer" : "approve/reject";
|
|
2951
|
+
return [{ id: requestId, label: kind, data: { selectedInboxRequestId: requestId }, message: `selected request ${requestId}; type ${kind} ${requestId}` }];
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
function runDetailSelectableItems(state) {
|
|
2955
|
+
const runId = selectedRunId(state);
|
|
2956
|
+
if (!runId)
|
|
2957
|
+
return [];
|
|
2958
|
+
return [
|
|
2959
|
+
{ id: "attach", label: "attach", intent: { scene: "handoff", argv: ["attach", runId], action: { kind: "run-attach", payload: { runId }, label: `Attach Pi ${runId.slice(0, 8)}` } }, message: "attach real bundled Pi" },
|
|
2960
|
+
{ id: "stop", label: "stop", intent: { scene: "run-detail", argv: ["stop", runId], action: { kind: "run-stop", payload: { runId }, label: `Stop ${runId.slice(0, 8)}` } }, message: "request stop" },
|
|
2961
|
+
{ id: "fleet", label: "fleet", intent: { scene: "fleet", argv: ["fleet"], action: { kind: "refresh", label: "Opening fleet" } }, message: "back to fleet" }
|
|
2962
|
+
];
|
|
2963
|
+
}
|
|
2964
|
+
function selectableItems(state) {
|
|
2965
|
+
switch (state.scene) {
|
|
2966
|
+
case "main":
|
|
2967
|
+
return mainSelectableItems(state);
|
|
2968
|
+
case "fleet":
|
|
2969
|
+
return fleetSelectableItems(state);
|
|
2970
|
+
case "tasks":
|
|
2971
|
+
return taskSelectableItems(state);
|
|
2972
|
+
case "inbox":
|
|
2973
|
+
return inboxSelectableItems(state);
|
|
2974
|
+
case "run-detail":
|
|
2975
|
+
return runDetailSelectableItems(state);
|
|
2976
|
+
default:
|
|
2977
|
+
return [];
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
function renderedItemsOrStateItems(state, renderedItems) {
|
|
2981
|
+
return renderedItems && renderedItems.length > 0 ? [...renderedItems] : selectableItems(state);
|
|
2982
|
+
}
|
|
2983
|
+
function selectedRenderedItemId(state) {
|
|
2984
|
+
if (state.scene === "fleet") {
|
|
2985
|
+
const selected = state.data.selectedRunId;
|
|
2986
|
+
return typeof selected === "string" && selected.trim() ? selected.trim() : undefined;
|
|
2987
|
+
}
|
|
2988
|
+
if (state.scene === "tasks") {
|
|
2989
|
+
const selected = state.data.selectedTaskId;
|
|
2990
|
+
return typeof selected === "string" && selected.trim() ? selected.trim() : undefined;
|
|
2991
|
+
}
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
function effectiveRenderedIndex(state, items) {
|
|
2995
|
+
if (items.length === 0)
|
|
2996
|
+
return -1;
|
|
2997
|
+
const selectedId = selectedRenderedItemId(state);
|
|
2998
|
+
if (selectedId) {
|
|
2999
|
+
const renderedIndex = items.findIndex((item) => item.id === selectedId);
|
|
3000
|
+
if (renderedIndex >= 0)
|
|
3001
|
+
return renderedIndex;
|
|
3002
|
+
if (state.scene === "fleet" || state.scene === "tasks")
|
|
3003
|
+
return 0;
|
|
3004
|
+
}
|
|
3005
|
+
return Math.max(0, Math.min(items.length - 1, state.selection.index));
|
|
3006
|
+
}
|
|
3007
|
+
function selectIndex(store, index, renderedItems) {
|
|
3008
|
+
const state = store.getState();
|
|
3009
|
+
const items = renderedItemsOrStateItems(state, renderedItems);
|
|
3010
|
+
if (items.length === 0)
|
|
3011
|
+
return false;
|
|
3012
|
+
const next = Math.max(0, Math.min(items.length - 1, index));
|
|
3013
|
+
const item = items[next];
|
|
3014
|
+
store.patch({
|
|
3015
|
+
selection: { index: next, count: items.length },
|
|
3016
|
+
...item.data ? { data: item.data } : {},
|
|
3017
|
+
typeBar: { message: item.message ?? item.label }
|
|
3018
|
+
});
|
|
3019
|
+
return true;
|
|
3020
|
+
}
|
|
3021
|
+
function moveSelection(store, delta, renderedItems) {
|
|
3022
|
+
const state = store.getState();
|
|
3023
|
+
const items = renderedItemsOrStateItems(state, renderedItems);
|
|
3024
|
+
const current = effectiveRenderedIndex(state, items);
|
|
3025
|
+
return current >= 0 && selectIndex(store, current + delta, items);
|
|
3026
|
+
}
|
|
3027
|
+
async function activateSelection(runtime, renderedItems) {
|
|
3028
|
+
const state = runtime.getState();
|
|
3029
|
+
const items = renderedItemsOrStateItems(state, renderedItems);
|
|
3030
|
+
const index = effectiveRenderedIndex(state, items);
|
|
3031
|
+
if (index < 0)
|
|
3032
|
+
return false;
|
|
3033
|
+
const item = items[index];
|
|
3034
|
+
if (item.data)
|
|
3035
|
+
runtime.patchState({ data: item.data });
|
|
3036
|
+
if (item.intent) {
|
|
3037
|
+
await runtime.runIntent(item.intent);
|
|
3038
|
+
return true;
|
|
3039
|
+
}
|
|
3040
|
+
runtime.patchState({ typeBar: { message: item.message ?? item.label } });
|
|
3041
|
+
return true;
|
|
3042
|
+
}
|
|
3043
|
+
async function launchRigOpenTuiApp(options) {
|
|
3044
|
+
const initialState = createInitialAppState({
|
|
3045
|
+
...options,
|
|
3046
|
+
projectConfigured: options.projectConfigured ?? detectProjectConfigured(options.projectRoot)
|
|
3047
|
+
});
|
|
3048
|
+
const store = createAppStore(initialState);
|
|
3049
|
+
const events = createAppEventBus();
|
|
3050
|
+
let renderer = null;
|
|
3051
|
+
let graphics = null;
|
|
3052
|
+
let imageVisuals = null;
|
|
3053
|
+
const scrollPanels = [];
|
|
3054
|
+
let typeBar = null;
|
|
3055
|
+
const textLines = [];
|
|
3056
|
+
let tickTimer = null;
|
|
3057
|
+
let destroyed = false;
|
|
3058
|
+
let runnerPromise = null;
|
|
3059
|
+
let renderedSelectableItems = [];
|
|
3060
|
+
const emitAsyncError = (label, error) => {
|
|
3061
|
+
const normalized = normalizeError(error);
|
|
3062
|
+
events.emit({ type: "action.failed", label, message: normalized.message, hint: normalized.hint, cause: normalized.cause });
|
|
3063
|
+
};
|
|
3064
|
+
const runAppAction = (label, action) => {
|
|
3065
|
+
action().catch((error) => emitAsyncError(label, error));
|
|
3066
|
+
};
|
|
3067
|
+
const runtime = {
|
|
3068
|
+
getState: () => store.getState(),
|
|
3069
|
+
patchState: (patch) => store.patch(patch),
|
|
3070
|
+
emit: (event) => events.emit(event),
|
|
3071
|
+
setScene: (scene, intent2) => events.emit({ type: "scene.change", scene, ...intent2 ? { intent: intent2 } : {} }),
|
|
3072
|
+
runIntent: async (nextIntent) => {
|
|
3073
|
+
events.emit({ type: "scene.change", scene: nextIntent.scene, intent: nextIntent });
|
|
3074
|
+
if (nextIntent.action.kind !== "none" && nextIntent.action.kind !== "navigate") {
|
|
3075
|
+
events.emit({ type: "action.started", label: nextIntent.action.label ?? nextIntent.action.kind, optimistic: { lastIntent: nextIntent } });
|
|
3076
|
+
}
|
|
3077
|
+
let handled = false;
|
|
3078
|
+
for (const adapter of options.adapters ?? []) {
|
|
3079
|
+
const result = await adapter.handleIntent?.(runtime, nextIntent);
|
|
3080
|
+
handled = handled || result === true;
|
|
3081
|
+
}
|
|
3082
|
+
if (!handled && nextIntent.action.kind !== "none" && nextIntent.action.kind !== "navigate") {
|
|
3083
|
+
events.emit({
|
|
3084
|
+
type: "action.progress",
|
|
3085
|
+
label: nextIntent.action.label ?? nextIntent.action.kind,
|
|
3086
|
+
detail: "adapter pending",
|
|
3087
|
+
data: { lastIntent: nextIntent }
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
},
|
|
3091
|
+
getRunnerContext: async () => {
|
|
3092
|
+
if (!options.initializeRuntime)
|
|
3093
|
+
throw new Error("Rig runtime initializer was not provided to the OpenTUI app.");
|
|
3094
|
+
runnerPromise ??= options.initializeRuntime();
|
|
3095
|
+
return runnerPromise;
|
|
3096
|
+
},
|
|
3097
|
+
suspend: () => renderer?.suspend(),
|
|
3098
|
+
resume: () => {
|
|
3099
|
+
renderer?.resume();
|
|
3100
|
+
if (renderer && !destroyed)
|
|
3101
|
+
renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, store.getState(), options.sceneRenderers);
|
|
3102
|
+
},
|
|
3103
|
+
destroy: () => renderer?.destroy()
|
|
3104
|
+
};
|
|
3105
|
+
events.subscribe((event) => store.patch(reduceAppEvent(store.getState(), event)));
|
|
3106
|
+
store.subscribe(() => {
|
|
3107
|
+
if (!destroyed && renderer)
|
|
3108
|
+
renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, store.getState(), options.sceneRenderers);
|
|
3109
|
+
});
|
|
3110
|
+
try {
|
|
3111
|
+
renderer = await createCliRenderer({
|
|
3112
|
+
screenMode: "alternate-screen",
|
|
3113
|
+
exitOnCtrlC: false,
|
|
3114
|
+
targetFps: 60,
|
|
3115
|
+
maxFps: 60,
|
|
3116
|
+
useMouse: true,
|
|
3117
|
+
autoFocus: false,
|
|
3118
|
+
useKittyKeyboard: {
|
|
3119
|
+
disambiguate: true,
|
|
3120
|
+
alternateKeys: true,
|
|
3121
|
+
events: false
|
|
3122
|
+
},
|
|
3123
|
+
consoleMode: "disabled",
|
|
3124
|
+
openConsoleOnError: false,
|
|
3125
|
+
backgroundColor: "transparent"
|
|
3126
|
+
});
|
|
3127
|
+
renderer.setTerminalTitle?.("Rig \u2014 operator cockpit");
|
|
3128
|
+
const layout = computeStageLayout(renderer.width, renderer.height);
|
|
3129
|
+
imageVisuals = forceModernImageVisuals() ? new ImageVisualLayer(process.stdout) : null;
|
|
3130
|
+
if (imageVisuals?.enabled) {
|
|
3131
|
+
renderer.setFrameCallback(async () => {
|
|
3132
|
+
imageVisuals?.flush();
|
|
3133
|
+
});
|
|
3134
|
+
}
|
|
3135
|
+
if (!imageVisuals?.enabled) {
|
|
3136
|
+
graphics = createGraphicsLayer(renderer, layout);
|
|
3137
|
+
renderer.root.add(graphics);
|
|
3138
|
+
}
|
|
3139
|
+
for (let index = 0;index < MAX_SCENE_PANELS; index += 1) {
|
|
3140
|
+
const panel = createScrollPanelLayer(renderer, `rig-scroll-panel-${index}`, (line2) => {
|
|
3141
|
+
const selectableIndex = line2.selectableIndex;
|
|
3142
|
+
if (selectableIndex === undefined)
|
|
3143
|
+
return;
|
|
3144
|
+
selectIndex(store, selectableIndex, renderedSelectableItems);
|
|
3145
|
+
});
|
|
3146
|
+
scrollPanels.push(panel);
|
|
3147
|
+
renderer.root.add(panel);
|
|
3148
|
+
}
|
|
3149
|
+
for (let index = 0;index < MAX_SCENE_LINES; index += 1) {
|
|
3150
|
+
const textLine = createTextLine(renderer, `rig-scene-line-${index}`, index, (line2) => {
|
|
3151
|
+
const selectableIndex = line2.selectableIndex;
|
|
3152
|
+
if (selectableIndex === undefined)
|
|
3153
|
+
return;
|
|
3154
|
+
selectIndex(store, selectableIndex, renderedSelectableItems);
|
|
3155
|
+
});
|
|
3156
|
+
textLines.push(textLine);
|
|
3157
|
+
renderer.root.add(textLine);
|
|
3158
|
+
}
|
|
3159
|
+
typeBar = createTypeBar(renderer);
|
|
3160
|
+
renderer.root.add(typeBar.prefix);
|
|
3161
|
+
renderer.root.add(typeBar.input);
|
|
3162
|
+
renderer.root.add(typeBar.footer);
|
|
3163
|
+
bindTypeBar(typeBar, {
|
|
3164
|
+
onInput(value) {
|
|
3165
|
+
const scene = store.getState().scene;
|
|
3166
|
+
events.emit({
|
|
3167
|
+
type: "typebar.patch",
|
|
3168
|
+
typeBar: {
|
|
3169
|
+
value,
|
|
3170
|
+
...isListSearchScene(scene) ? { message: value.trim() ? `search ${JSON.stringify(value.trim())}` : undefined } : {}
|
|
3171
|
+
}
|
|
3172
|
+
});
|
|
3173
|
+
},
|
|
3174
|
+
onEnter(value) {
|
|
3175
|
+
const scene = store.getState().scene;
|
|
3176
|
+
if (isListSearchScene(scene) && value.trim()) {
|
|
3177
|
+
if (renderedSelectableItems.length === 0) {
|
|
3178
|
+
events.emit({ type: "typebar.patch", typeBar: { message: "no search match" } });
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
const snapshot = renderedSelectableItems;
|
|
3182
|
+
typeBar?.input && (typeBar.input.value = "");
|
|
3183
|
+
events.emit({ type: "typebar.patch", typeBar: { value: "" } });
|
|
3184
|
+
runAppAction("Activate search result", () => activateSelection(runtime, snapshot));
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
typeBar?.input && (typeBar.input.value = "");
|
|
3188
|
+
events.emit({ type: "typebar.patch", typeBar: { value: "" } });
|
|
3189
|
+
if (value.trim() === "") {
|
|
3190
|
+
runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
runAppAction("Run command", () => runtime.runIntent(intentFromTypeBar(value)));
|
|
3194
|
+
}
|
|
3195
|
+
});
|
|
3196
|
+
renderer.keyInput.on("keypress", (key) => {
|
|
3197
|
+
const host = getActivePiHost();
|
|
3198
|
+
if (host && store.getState().scene === "handoff") {
|
|
3199
|
+
if (key.ctrl && key.name === "]") {
|
|
3200
|
+
stopActivePiHost("operator detach");
|
|
3201
|
+
events.emit({ type: "typebar.patch", typeBar: { value: "", message: "detached from Pi" } });
|
|
3202
|
+
runAppAction("Opening runs", () => runtime.runIntent({ scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } }));
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
const sequence = key.raw || key.sequence;
|
|
3206
|
+
if (sequence)
|
|
3207
|
+
host.write(sequence);
|
|
3208
|
+
if (typeBar)
|
|
3209
|
+
typeBar.input.value = "";
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
if (key.ctrl && key.name === "c") {
|
|
3213
|
+
renderer?.destroy();
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
const currentScene = store.getState().scene;
|
|
3217
|
+
const searchScene = isListSearchScene(currentScene);
|
|
3218
|
+
const inputValue = typeBar?.input.value ?? "";
|
|
3219
|
+
const typeBarEmpty = inputValue.trim() === "";
|
|
3220
|
+
if (typeBarEmpty && (key.name === "right" || key.name === "tab")) {
|
|
3221
|
+
runAppAction("Navigate", () => runtime.runIntent(primaryNavIntent(nextPrimaryScene(store.getState().scene, 1))));
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
if (typeBarEmpty && key.name === "left") {
|
|
3225
|
+
runAppAction("Navigate", () => runtime.runIntent(primaryNavIntent(nextPrimaryScene(store.getState().scene, -1))));
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
if (!searchScene && typeBarEmpty && key.name === "r") {
|
|
3229
|
+
runAppAction("Opening runs", () => runtime.runIntent(primaryNavIntent("fleet")));
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
if (!searchScene && typeBarEmpty && key.name === "t") {
|
|
3233
|
+
runAppAction("Opening tasks", () => runtime.runIntent(primaryNavIntent("tasks")));
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
if (!searchScene && typeBarEmpty && (key.name === "h" || key.sequence === "?")) {
|
|
3237
|
+
runAppAction("Opening help", () => runtime.runIntent(primaryNavIntent("help")));
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
if (searchScene && key.name === "up" || !searchScene && typeBarEmpty && (key.name === "up" || key.name === "k")) {
|
|
3241
|
+
if (moveSelection(store, -1, renderedSelectableItems))
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
if (searchScene && key.name === "down" || !searchScene && typeBarEmpty && (key.name === "down" || key.name === "j")) {
|
|
3245
|
+
if (moveSelection(store, 1, renderedSelectableItems))
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
if (typeBarEmpty && (key.name === "return" || key.name === "enter")) {
|
|
3249
|
+
runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
if (!searchScene && key.name === "q" && typeBarEmpty) {
|
|
3253
|
+
renderer?.destroy();
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
if (key.name === "escape") {
|
|
3257
|
+
if (inputValue) {
|
|
3258
|
+
if (typeBar)
|
|
3259
|
+
typeBar.input.value = "";
|
|
3260
|
+
events.emit({ type: "typebar.patch", typeBar: { value: "", message: undefined } });
|
|
3261
|
+
} else {
|
|
3262
|
+
runAppAction("Opening runs", () => runtime.runIntent({ scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } }));
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
});
|
|
3266
|
+
renderer.keyInput.on("paste", (event) => {
|
|
3267
|
+
const host = getActivePiHost();
|
|
3268
|
+
if (!host || store.getState().scene !== "handoff")
|
|
3269
|
+
return;
|
|
3270
|
+
host.write(event.bytes);
|
|
3271
|
+
if (typeBar)
|
|
3272
|
+
typeBar.input.value = "";
|
|
3273
|
+
});
|
|
3274
|
+
renderer.on("selection", (selection) => {
|
|
3275
|
+
const selectedText = selection.getSelectedText();
|
|
3276
|
+
if (!selectedText)
|
|
3277
|
+
return;
|
|
3278
|
+
renderer?.copyToClipboardOSC52?.(selectedText);
|
|
3279
|
+
events.emit({ type: "typebar.patch", typeBar: { message: "copied selection" } });
|
|
3280
|
+
});
|
|
3281
|
+
renderer.on("resize", () => {
|
|
3282
|
+
if (renderer)
|
|
3283
|
+
renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, store.getState(), options.sceneRenderers);
|
|
3284
|
+
});
|
|
3285
|
+
renderer.on("destroy", () => {
|
|
3286
|
+
destroyed = true;
|
|
3287
|
+
stopActivePiHost("renderer destroyed");
|
|
3288
|
+
imageVisuals?.destroy();
|
|
3289
|
+
if (tickTimer)
|
|
3290
|
+
clearInterval(tickTimer);
|
|
3291
|
+
tickTimer = null;
|
|
3292
|
+
});
|
|
3293
|
+
renderer.requestLive?.();
|
|
3294
|
+
renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, store.getState(), options.sceneRenderers);
|
|
3295
|
+
requestRender(renderer);
|
|
3296
|
+
queueMicrotask(() => {
|
|
3297
|
+
(async () => {
|
|
3298
|
+
try {
|
|
3299
|
+
if (options.initializeRuntime) {
|
|
3300
|
+
const runner = await runtime.getRunnerContext();
|
|
3301
|
+
events.emit({ type: "runtime.ready", runner });
|
|
3302
|
+
}
|
|
3303
|
+
for (const adapter of options.adapters ?? [])
|
|
3304
|
+
await adapter.start?.(runtime);
|
|
3305
|
+
await runtime.runIntent(store.getState().intent);
|
|
3306
|
+
} catch (error) {
|
|
3307
|
+
const normalized = normalizeError(error);
|
|
3308
|
+
events.emit({ type: "runtime.failed", message: normalized.message, hint: normalized.hint, cause: normalized.cause });
|
|
3309
|
+
}
|
|
3310
|
+
})();
|
|
3311
|
+
});
|
|
3312
|
+
tickTimer = setInterval(() => {
|
|
3313
|
+
if (destroyed)
|
|
3314
|
+
return;
|
|
3315
|
+
const state = store.getState();
|
|
3316
|
+
if (state.scene === "handoff" && getActivePiHost())
|
|
3317
|
+
return;
|
|
3318
|
+
store.patch({ tick: state.tick + 1 });
|
|
3319
|
+
}, FRAME_MS);
|
|
3320
|
+
await new Promise((resolveDestroy) => renderer?.on("destroy", resolveDestroy));
|
|
3321
|
+
} finally {
|
|
3322
|
+
destroyed = true;
|
|
3323
|
+
if (tickTimer)
|
|
3324
|
+
clearInterval(tickTimer);
|
|
3325
|
+
tickTimer = null;
|
|
3326
|
+
imageVisuals?.destroy();
|
|
3327
|
+
stopActivePiHost("app shutdown");
|
|
3328
|
+
try {
|
|
3329
|
+
renderer?.dropLive?.();
|
|
3330
|
+
} catch {}
|
|
3331
|
+
if (renderer && !renderer.isDestroyed)
|
|
3332
|
+
renderer.destroy();
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
function renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, state, renderers) {
|
|
3336
|
+
const layout = computeStageLayout(renderer.width, renderer.height);
|
|
3337
|
+
const animationTick = Math.floor(state.tick / ANIMATION_SLOWDOWN);
|
|
3338
|
+
const renderState = animationTick === state.tick ? state : { ...state, tick: animationTick };
|
|
3339
|
+
const frame = rendererForScene(renderState.scene, renderers)(renderState, layout);
|
|
3340
|
+
if (frame.terminalActive) {
|
|
3341
|
+
getActivePiHost()?.resize(layout.width, Math.max(1, layout.height));
|
|
3342
|
+
}
|
|
3343
|
+
if (graphics) {
|
|
3344
|
+
resizeGraphicsLayer(graphics, layout);
|
|
3345
|
+
if (frame.terminalActive || frame.fullScreen)
|
|
3346
|
+
clearGraphicsLayer(graphics);
|
|
3347
|
+
else
|
|
3348
|
+
drawAmbientField(graphics, layout, animationTick, state.scene);
|
|
3349
|
+
}
|
|
3350
|
+
if (imageVisuals && (frame.terminalActive || frame.fullScreen)) {
|
|
3351
|
+
imageVisuals.clear();
|
|
3352
|
+
}
|
|
3353
|
+
const contentLayout = frame.fullScreen ? { ...layout, centerWidth: layout.width, centerLeft: 0, centerTop: 0, centerHeight: Math.min(layout.height, MAX_SCENE_LINES) } : layout;
|
|
3354
|
+
const sceneLines = fitSceneLines(frame.lines, contentLayout);
|
|
3355
|
+
const allSelectableItems = selectableItems(state);
|
|
3356
|
+
const selectableIndexMap = new Map;
|
|
3357
|
+
const renderedSelectableItems = [];
|
|
3358
|
+
const remapSelectableLine = (entry) => {
|
|
3359
|
+
if (entry.selectableIndex === undefined)
|
|
3360
|
+
return entry;
|
|
3361
|
+
const item = allSelectableItems[entry.selectableIndex];
|
|
3362
|
+
if (!item)
|
|
3363
|
+
return { ...entry, selectableIndex: undefined };
|
|
3364
|
+
let renderedIndex = selectableIndexMap.get(entry.selectableIndex);
|
|
3365
|
+
if (renderedIndex === undefined) {
|
|
3366
|
+
renderedIndex = renderedSelectableItems.length;
|
|
3367
|
+
selectableIndexMap.set(entry.selectableIndex, renderedIndex);
|
|
3368
|
+
renderedSelectableItems.push(item);
|
|
3369
|
+
}
|
|
3370
|
+
return { ...entry, selectableIndex: renderedIndex };
|
|
3371
|
+
};
|
|
3372
|
+
const renderedSceneLines = sceneLines.map(remapSelectableLine);
|
|
3373
|
+
const renderedPanels = (frame.panels ?? []).map((panel) => ({
|
|
3374
|
+
...panel,
|
|
3375
|
+
lines: panel.lines.map(remapSelectableLine)
|
|
3376
|
+
}));
|
|
3377
|
+
if (imageVisuals && !frame.terminalActive && !frame.fullScreen) {
|
|
3378
|
+
imageVisuals.render({
|
|
3379
|
+
layout: contentLayout,
|
|
3380
|
+
pixelSize: pixelSizeForRenderer(renderer),
|
|
3381
|
+
panels: renderedPanels,
|
|
3382
|
+
scene: state.scene,
|
|
3383
|
+
tick: state.tick
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
if (graphics && !frame.terminalActive)
|
|
3387
|
+
drawPanelChrome(graphics, contentLayout, renderedPanels);
|
|
3388
|
+
renderedSceneLines.forEach((entry, index) => applyTextLine(textLines[index], entry, contentLayout.centerTop + index, contentLayout.centerLeft, contentLayout.centerWidth));
|
|
3389
|
+
clearTextLines(textLines, renderedSceneLines.length);
|
|
3390
|
+
applyScrollPanels(scrollPanels, contentLayout, renderedPanels);
|
|
3391
|
+
if (typeBar) {
|
|
3392
|
+
if (frame.hideTypeBar) {
|
|
3393
|
+
hideTypeBar(typeBar, layout.height);
|
|
3394
|
+
} else {
|
|
3395
|
+
positionTypeBar(typeBar, {
|
|
3396
|
+
...state.typeBar,
|
|
3397
|
+
placeholder: frame.typeBarPlaceholder ?? state.typeBar.placeholder
|
|
3398
|
+
}, { ...state.footer, ...frame.footer ?? {} }, layout.width, layout.typeBarTop, layout.footerTop);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
requestRender(renderer);
|
|
3402
|
+
return renderedSelectableItems;
|
|
3403
|
+
}
|
|
3404
|
+
export {
|
|
3405
|
+
launchRigOpenTuiApp
|
|
3406
|
+
};
|