@zenbu-labs/pixel 0.0.0 → 0.0.13
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/dist/bin.d.ts +2 -0
- package/dist/bin.js +161 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +75 -0
- package/dist/context-menu.d.ts +20 -0
- package/dist/context-menu.js +57 -0
- package/dist/debug.d.ts +1 -0
- package/dist/debug.js +9 -0
- package/dist/devtools-layout.d.ts +21 -0
- package/dist/devtools-layout.js +45 -0
- package/dist/devtools.d.ts +33 -0
- package/dist/devtools.js +125 -0
- package/dist/electron/index.d.ts +3 -0
- package/dist/electron/index.js +4 -0
- package/dist/host/guest.d.ts +5 -0
- package/dist/host/guest.js +178 -0
- package/dist/host/server.d.ts +91 -0
- package/dist/host/server.js +296 -0
- package/dist/host/shell.d.ts +35 -0
- package/dist/host/shell.js +113 -0
- package/dist/host/strip.d.ts +6 -0
- package/dist/host/strip.js +63 -0
- package/dist/icons.d.ts +30 -0
- package/dist/icons.js +58 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +39 -0
- package/dist/instances.d.ts +30 -0
- package/dist/instances.js +177 -0
- package/dist/layout.d.ts +5 -0
- package/dist/layout.js +34 -0
- package/dist/popup.d.ts +21 -0
- package/dist/popup.js +47 -0
- package/dist/preload/index.d.ts +15 -0
- package/dist/preload/index.js +3 -0
- package/dist/react/colors.d.ts +3 -0
- package/dist/react/colors.js +25 -0
- package/dist/react/components.d.ts +21 -0
- package/dist/react/components.js +45 -0
- package/dist/react/devtools/app.d.ts +3 -0
- package/dist/react/devtools/app.js +89 -0
- package/dist/react/devtools/console-capture.d.ts +1 -0
- package/dist/react/devtools/console-capture.js +41 -0
- package/dist/react/devtools/controller.d.ts +45 -0
- package/dist/react/devtools/controller.js +252 -0
- package/dist/react/devtools/elements.d.ts +4 -0
- package/dist/react/devtools/elements.js +152 -0
- package/dist/react/devtools/export-profile.d.ts +2 -0
- package/dist/react/devtools/export-profile.js +225 -0
- package/dist/react/devtools/fiber-hook.d.ts +1 -0
- package/dist/react/devtools/fiber-hook.js +89 -0
- package/dist/react/devtools/log-panel.d.ts +4 -0
- package/dist/react/devtools/log-panel.js +113 -0
- package/dist/react/devtools/profiler.d.ts +11 -0
- package/dist/react/devtools/profiler.js +374 -0
- package/dist/react/devtools/store.d.ts +27 -0
- package/dist/react/devtools/store.js +61 -0
- package/dist/react/devtools/stores.d.ts +92 -0
- package/dist/react/devtools/stores.js +41 -0
- package/dist/react/devtools/theme.d.ts +35 -0
- package/dist/react/devtools/theme.js +77 -0
- package/dist/react/devtools/ui.d.ts +24 -0
- package/dist/react/devtools/ui.js +59 -0
- package/dist/react/index.d.ts +110 -0
- package/dist/react/index.js +538 -0
- package/dist/react/markdown.d.ts +25 -0
- package/dist/react/markdown.js +270 -0
- package/dist/react/native.d.ts +116 -0
- package/dist/react/native.js +56 -0
- package/dist/react/reconciler-config.d.ts +241 -0
- package/dist/react/reconciler-config.js +412 -0
- package/dist/react/styles.d.ts +88 -0
- package/dist/react/styles.js +85 -0
- package/dist/react/surface.d.ts +55 -0
- package/dist/react/surface.js +74 -0
- package/dist/registry.d.ts +53 -0
- package/dist/registry.js +193 -0
- package/dist/root.d.ts +19 -0
- package/dist/root.js +342 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/sandbox.js +84 -0
- package/dist/scale.d.ts +7 -0
- package/dist/scale.js +31 -0
- package/dist/ssh/index.d.ts +58 -0
- package/dist/ssh/index.js +445 -0
- package/dist/terminal/applescript.d.ts +2 -0
- package/dist/terminal/applescript.js +20 -0
- package/dist/terminal/detect.d.ts +10 -0
- package/dist/terminal/detect.js +34 -0
- package/dist/terminal/graphics.d.ts +9 -0
- package/dist/terminal/graphics.js +142 -0
- package/dist/terminal/index.d.ts +12 -0
- package/dist/terminal/index.js +23 -0
- package/dist/terminal/run.d.ts +2 -0
- package/dist/terminal/run.js +15 -0
- package/dist/terminal/shared.d.ts +20 -0
- package/dist/terminal/shared.js +88 -0
- package/dist/terminal/terminal.d.ts +66 -0
- package/dist/terminal/terminal.js +6 -0
- package/dist/terminal/terminals/cmux.d.ts +2 -0
- package/dist/terminal/terminals/cmux.js +98 -0
- package/dist/terminal/terminals/ghostty-script.d.ts +1 -0
- package/dist/terminal/terminals/ghostty-script.js +179 -0
- package/dist/terminal/terminals/ghostty.d.ts +2 -0
- package/dist/terminal/terminals/ghostty.js +336 -0
- package/dist/terminal/terminals/herdr.d.ts +2 -0
- package/dist/terminal/terminals/herdr.js +141 -0
- package/dist/terminal/terminals/index.d.ts +5 -0
- package/dist/terminal/terminals/index.js +26 -0
- package/dist/terminal/terminals/kitty.d.ts +2 -0
- package/dist/terminal/terminals/kitty.js +209 -0
- package/dist/terminal/terminals/supacode.d.ts +2 -0
- package/dist/terminal/terminals/supacode.js +79 -0
- package/dist/terminal/terminals/tmux.d.ts +2 -0
- package/dist/terminal/terminals/tmux.js +132 -0
- package/dist/terminal/terminals/tty7.d.ts +2 -0
- package/dist/terminal/terminals/tty7.js +58 -0
- package/dist/terminal/terminals/vscode.d.ts +2 -0
- package/dist/terminal/terminals/vscode.js +13 -0
- package/dist/terminal/terminals/wezterm.d.ts +2 -0
- package/dist/terminal/terminals/wezterm.js +59 -0
- package/dist/theme.d.ts +22 -0
- package/dist/theme.js +40 -0
- package/dist/web/browser-session.d.ts +13 -0
- package/dist/web/browser-session.js +186 -0
- package/dist/web/cursor.d.ts +1 -0
- package/dist/web/cursor.js +24 -0
- package/dist/web/devtools.d.ts +39 -0
- package/dist/web/devtools.js +300 -0
- package/dist/web/favicon.d.ts +7 -0
- package/dist/web/favicon.js +73 -0
- package/dist/web/frame-rate.d.ts +1 -0
- package/dist/web/frame-rate.js +12 -0
- package/dist/web/host.d.ts +123 -0
- package/dist/web/host.js +573 -0
- package/dist/web/input.d.ts +49 -0
- package/dist/web/input.js +468 -0
- package/dist/web/offscreen.d.ts +2 -0
- package/dist/web/offscreen.js +23 -0
- package/dist/web/paint.d.ts +11 -0
- package/dist/web/paint.js +128 -0
- package/dist/web/popup.d.ts +40 -0
- package/dist/web/popup.js +144 -0
- package/dist/web/session.d.ts +20 -0
- package/dist/web/session.js +110 -0
- package/dist/web/types.d.ts +38 -0
- package/dist/web/types.js +39 -0
- package/dist/web/zoom.d.ts +5 -0
- package/dist/web/zoom.js +36 -0
- package/dist/webview.d.ts +55 -0
- package/dist/webview.js +481 -0
- package/package.json +69 -3
- package/scripts/apparmor.sh +71 -0
- package/scripts/postinstall.mjs +150 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GHOSTTY_SCRIPT = void 0;
|
|
4
|
+
exports.GHOSTTY_SCRIPT = String.raw `ObjC.import("Foundation");
|
|
5
|
+
|
|
6
|
+
const Descriptor = $.NSAppleEventDescriptor;
|
|
7
|
+
const WAIT_FOR_REPLY = 3;
|
|
8
|
+
const NOT_FOUND = -1728;
|
|
9
|
+
|
|
10
|
+
function code(text) {
|
|
11
|
+
return ((text.charCodeAt(0) << 24) | (text.charCodeAt(1) << 16) | (text.charCodeAt(2) << 8) | text.charCodeAt(3)) >>> 0;
|
|
12
|
+
}
|
|
13
|
+
const typeCode = (text) => Descriptor.descriptorWithTypeCode(code(text));
|
|
14
|
+
const enumCode = (text) => Descriptor.descriptorWithEnumCode(code(text));
|
|
15
|
+
const string = (text) => Descriptor.descriptorWithString(text);
|
|
16
|
+
const boolean = (value) => Descriptor.descriptorWithBoolean(value);
|
|
17
|
+
const nothing = Descriptor.nullDescriptor;
|
|
18
|
+
// the "every element" ordinal is the four-char code 'all ' stored in native (little-endian) byte order
|
|
19
|
+
const everyOrdinal = Descriptor.descriptorWithDescriptorTypeData(
|
|
20
|
+
code("abso"),
|
|
21
|
+
$(" lla").dataUsingEncoding($.NSMacOSRomanStringEncoding),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
function specifier(wanted, container, form, key) {
|
|
25
|
+
const record = Descriptor.recordDescriptor;
|
|
26
|
+
record.setDescriptorForKeyword(typeCode(wanted), code("want"));
|
|
27
|
+
record.setDescriptorForKeyword(container, code("from"));
|
|
28
|
+
record.setDescriptorForKeyword(enumCode(form), code("form"));
|
|
29
|
+
record.setDescriptorForKeyword(key, code("seld"));
|
|
30
|
+
return record.coerceToDescriptorType(code("obj "));
|
|
31
|
+
}
|
|
32
|
+
const every = (cls, container) => specifier(cls, container, "indx", everyOrdinal);
|
|
33
|
+
const property = (name, of) => specifier("prop", of, "prop", typeCode(name));
|
|
34
|
+
const terminal = (id) => specifier("Gtrm", nothing, "ID ", string(id));
|
|
35
|
+
const window = (id) => specifier("Gwnd", nothing, "ID ", string(id));
|
|
36
|
+
// tabs are only reachable through their window, so a tab needs both ids
|
|
37
|
+
const tab = (windowId, id) => specifier("Gtab", window(windowId), "ID ", string(id));
|
|
38
|
+
|
|
39
|
+
function unwrap(descriptor) {
|
|
40
|
+
if (descriptor.isNil()) return null;
|
|
41
|
+
const type = descriptor.descriptorType;
|
|
42
|
+
if (type === code("list")) {
|
|
43
|
+
const items = [];
|
|
44
|
+
for (let i = 1; i <= descriptor.numberOfItems; i++) items.push(unwrap(descriptor.descriptorAtIndex(i)));
|
|
45
|
+
return items;
|
|
46
|
+
}
|
|
47
|
+
if (type === code("bool") || type === code("true") || type === code("fals")) return descriptor.booleanValue;
|
|
48
|
+
const text = descriptor.stringValue;
|
|
49
|
+
return text.isNil() ? null : ObjC.unwrap(text);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function send(pid, eventClass, eventId, params) {
|
|
53
|
+
const event = Descriptor.appleEventWithEventClassEventIDTargetDescriptorReturnIDTransactionID(
|
|
54
|
+
code(eventClass),
|
|
55
|
+
code(eventId),
|
|
56
|
+
Descriptor.descriptorWithProcessIdentifier(pid),
|
|
57
|
+
-1,
|
|
58
|
+
0,
|
|
59
|
+
);
|
|
60
|
+
for (const keyword of Object.keys(params)) event.setParamDescriptorForKeyword(params[keyword], code(keyword));
|
|
61
|
+
const failure = Ref();
|
|
62
|
+
const reply = event.sendEventWithOptionsTimeoutError(WAIT_FOR_REPLY, 10, failure);
|
|
63
|
+
if (reply.isNil()) {
|
|
64
|
+
throw new Error("[placeholder copy: Ghostty (pid " + pid + ") did not answer: " + ObjC.unwrap(failure[0].localizedDescription) + "]");
|
|
65
|
+
}
|
|
66
|
+
const number = reply.paramDescriptorForKeyword(code("errn"));
|
|
67
|
+
if (!number.isNil()) {
|
|
68
|
+
const message = reply.paramDescriptorForKeyword(code("errs"));
|
|
69
|
+
const detail = message.isNil() ? "" : ": " + ObjC.unwrap(message.stringValue);
|
|
70
|
+
const error = new Error("[placeholder copy: Ghostty (pid " + pid + ") refused the request (" + number.int32Value + ")" + detail + "]");
|
|
71
|
+
error.code = number.int32Value;
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return reply.paramDescriptorForKeyword(code("----"));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const get = (pid, name, of) => unwrap(send(pid, "core", "getd", { "----": property(name, of) }));
|
|
78
|
+
const performAction = (pid, action, on) => unwrap(send(pid, "Ghst", "PfAc", { "----": string(action), GonT: on }));
|
|
79
|
+
|
|
80
|
+
function list(pid) {
|
|
81
|
+
const windows = every("Gwnd", nothing);
|
|
82
|
+
const tabs = every("Gtab", windows);
|
|
83
|
+
const terminals = every("Gtrm", tabs);
|
|
84
|
+
const optional = (name) => {
|
|
85
|
+
try {
|
|
86
|
+
return get(pid, name, terminals);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error.code === NOT_FOUND) return null;
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const windowIds = get(pid, "ID ", windows);
|
|
93
|
+
const tabIds = get(pid, "ID ", tabs);
|
|
94
|
+
const ids = get(pid, "ID ", terminals);
|
|
95
|
+
const cwds = get(pid, "Gwdr", terminals);
|
|
96
|
+
const foregroundPids = optional("Gpid");
|
|
97
|
+
const ttys = optional("Gtty");
|
|
98
|
+
let out = "";
|
|
99
|
+
windowIds.forEach((windowId, w) => {
|
|
100
|
+
tabIds[w].forEach((tabId, t) => {
|
|
101
|
+
ids[w][t].forEach((id, i) => {
|
|
102
|
+
const foreground = foregroundPids ? foregroundPids[w][t][i] : "";
|
|
103
|
+
const tty = ttys ? ttys[w][t][i] : "";
|
|
104
|
+
out += [windowId, tabId, id, foreground ?? "", tty ?? "", cwds[w][t][i] ?? ""].join("\t") + "\n";
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Ghostty does not report where splits sit, but goto_split says whether it could
|
|
112
|
+
// move and the tab says where focus landed. Focus moves a moment after each
|
|
113
|
+
// action returns, so every move is waited for; focus is put back where it was.
|
|
114
|
+
function neighbor(pid, windowId, tabId, id, direction) {
|
|
115
|
+
const inTab = tab(windowId, tabId);
|
|
116
|
+
const focused = () => get(pid, "ID ", property("GTfT", inTab));
|
|
117
|
+
const settle = (until) => {
|
|
118
|
+
for (let i = 0; i < 50; i++) {
|
|
119
|
+
const now = focused();
|
|
120
|
+
if (until(now)) return now;
|
|
121
|
+
delay(0.02);
|
|
122
|
+
}
|
|
123
|
+
return focused();
|
|
124
|
+
};
|
|
125
|
+
const focus = (target) => {
|
|
126
|
+
send(pid, "Ghst", "Fcus", { "----": terminal(target) });
|
|
127
|
+
settle((now) => now === target);
|
|
128
|
+
};
|
|
129
|
+
const startId = focused();
|
|
130
|
+
if (startId !== id) focus(id);
|
|
131
|
+
const moved = performAction(pid, "goto_split:" + direction, terminal(id)) === true;
|
|
132
|
+
const endId = moved ? settle((now) => now !== id) : id;
|
|
133
|
+
if (focused() !== startId) focus(startId);
|
|
134
|
+
return moved && endId !== id ? endId : "";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function orNotFound(work) {
|
|
138
|
+
try {
|
|
139
|
+
return work();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error.code === NOT_FOUND) return "not-found";
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function run(argv) {
|
|
147
|
+
const [pid, command, ...rest] = argv;
|
|
148
|
+
const target = Number(pid);
|
|
149
|
+
switch (command) {
|
|
150
|
+
case "list":
|
|
151
|
+
return list(target);
|
|
152
|
+
case "split":
|
|
153
|
+
return orNotFound(() => {
|
|
154
|
+
const [id, direction, directory, command] = rest;
|
|
155
|
+
const configuration = Descriptor.recordDescriptor;
|
|
156
|
+
configuration.setDescriptorForKeyword(string(directory), code("GScD"));
|
|
157
|
+
configuration.setDescriptorForKeyword(string(command), code("GScC"));
|
|
158
|
+
configuration.setDescriptorForKeyword(boolean(false), code("GScW"));
|
|
159
|
+
const opened = send(target, "Ghst", "Splt", { "----": terminal(id), GSpd: enumCode(direction), GSpS: configuration });
|
|
160
|
+
return get(target, "ID ", opened);
|
|
161
|
+
});
|
|
162
|
+
case "input":
|
|
163
|
+
return orNotFound(() => {
|
|
164
|
+
send(target, "Ghst", "InTx", { "----": string(rest[1]), GItT: terminal(rest[0]) });
|
|
165
|
+
return "ok";
|
|
166
|
+
});
|
|
167
|
+
case "focus":
|
|
168
|
+
return orNotFound(() => {
|
|
169
|
+
send(target, "Ghst", "Fcus", { "----": terminal(rest[0]) });
|
|
170
|
+
return "ok";
|
|
171
|
+
});
|
|
172
|
+
case "action":
|
|
173
|
+
return orNotFound(() => String(performAction(target, rest[1], terminal(rest[0]))));
|
|
174
|
+
case "neighbor":
|
|
175
|
+
return orNotFound(() => neighbor(target, rest[0], rest[1], rest[2], rest[3]));
|
|
176
|
+
}
|
|
177
|
+
throw new Error("unknown command " + command);
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ghostty = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const applescript_1 = require("../applescript");
|
|
11
|
+
const ghostty_script_1 = require("./ghostty-script");
|
|
12
|
+
const graphics_1 = require("../graphics");
|
|
13
|
+
const shared_1 = require("../shared");
|
|
14
|
+
const DIRECTION_CODES = {
|
|
15
|
+
right: "GSrt",
|
|
16
|
+
left: "GSlf",
|
|
17
|
+
down: "GSdn",
|
|
18
|
+
up: "GSup",
|
|
19
|
+
};
|
|
20
|
+
const GHOSTTY_BINARY = /\/Ghostty\.app\/Contents\/MacOS\/ghostty(\s|$)/;
|
|
21
|
+
function ghosttyAbove(start, processes) {
|
|
22
|
+
for (let pid = start, hops = 0; hops < 64 && processes.has(pid); hops++) {
|
|
23
|
+
const { parent, command } = processes.get(pid);
|
|
24
|
+
if (GHOSTTY_BINARY.test(command))
|
|
25
|
+
return pid;
|
|
26
|
+
pid = parent;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function markerDirectory() {
|
|
31
|
+
const name = `pixel-pane-${process.pid}-`;
|
|
32
|
+
try {
|
|
33
|
+
return node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), name));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return node_path_1.default.join(node_os_1.default.tmpdir(), name + Math.floor(Math.random() * 1e9));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const ghostty = (env, run) => {
|
|
40
|
+
const looksLikeGhostty = (env.TERM ?? "").includes("ghostty") ||
|
|
41
|
+
env.TERM_PROGRAM === "ghostty" ||
|
|
42
|
+
Boolean(env.GHOSTTY_RESOURCES_DIR);
|
|
43
|
+
if (!looksLikeGhostty)
|
|
44
|
+
return null;
|
|
45
|
+
if (process.platform !== "darwin") {
|
|
46
|
+
return {
|
|
47
|
+
name: "ghostty",
|
|
48
|
+
async split() {
|
|
49
|
+
throw new Error("--split is not supported inside ghostty on this platform");
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const tooOld = (env.TERM_PROGRAM_VERSION?.localeCompare("1.3.0", undefined, { numeric: true }) ?? 0) < 0;
|
|
54
|
+
if (tooOld) {
|
|
55
|
+
return {
|
|
56
|
+
name: "ghostty",
|
|
57
|
+
async split() {
|
|
58
|
+
throw new Error(`Ghostty ${env.TERM_PROGRAM_VERSION} does not support automation: upgrade to Ghostty 1.3.0 or newer.`);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const osascript = (0, applescript_1.appleScript)("Ghostty", run, "JavaScript");
|
|
63
|
+
const directories = new Map();
|
|
64
|
+
let owner = null;
|
|
65
|
+
function ownerPid(tty) {
|
|
66
|
+
owner ??= findOwner(tty).catch((error) => {
|
|
67
|
+
owner = null;
|
|
68
|
+
throw error;
|
|
69
|
+
});
|
|
70
|
+
return owner;
|
|
71
|
+
}
|
|
72
|
+
async function ghosttyAncestor() {
|
|
73
|
+
for (let pid = process.pid, hops = 0; pid > 1 && hops < 64; hops++) {
|
|
74
|
+
const line = await run("ps", ["-o", "pid=,ppid=,command=", "-p", String(pid)]).catch(() => "");
|
|
75
|
+
const parts = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
76
|
+
if (!parts)
|
|
77
|
+
return null;
|
|
78
|
+
if (GHOSTTY_BINARY.test(parts[3]))
|
|
79
|
+
return pid;
|
|
80
|
+
pid = Number(parts[2]);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
async function findOwner(tty) {
|
|
85
|
+
const ancestor = await ghosttyAncestor();
|
|
86
|
+
if (ancestor !== null)
|
|
87
|
+
return ancestor;
|
|
88
|
+
const processes = new Map();
|
|
89
|
+
for (const line of (await run("ps", ["-axo", "pid=,ppid=,tty=,command="])).split("\n")) {
|
|
90
|
+
const parts = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
|
|
91
|
+
if (parts)
|
|
92
|
+
processes.set(Number(parts[1]), { parent: Number(parts[2]), tty: parts[3], command: parts[4] });
|
|
93
|
+
}
|
|
94
|
+
const above = ghosttyAbove(process.pid, processes);
|
|
95
|
+
if (above !== null)
|
|
96
|
+
return above;
|
|
97
|
+
if (tty) {
|
|
98
|
+
for (const [pid, { tty: owned }] of processes) {
|
|
99
|
+
if (`/dev/${owned}` !== tty)
|
|
100
|
+
continue;
|
|
101
|
+
const found = ghosttyAbove(pid, processes);
|
|
102
|
+
if (found !== null)
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const running = [...processes].filter(([, { command }]) => GHOSTTY_BINARY.test(command)).map(([pid]) => pid);
|
|
107
|
+
if (running.length === 1)
|
|
108
|
+
return running[0];
|
|
109
|
+
if (running.length === 0) {
|
|
110
|
+
throw new Error("[placeholder copy: Ghostty is not running, so there is no window to script]");
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`[placeholder copy: ${running.length} Ghostty processes are running (pids ${running.join(", ")}) and this shell is not inside any of them, so we cannot tell which one to script]`);
|
|
113
|
+
}
|
|
114
|
+
async function ghosttyCommand(command, args, tty = null) {
|
|
115
|
+
return osascript(ghostty_script_1.GHOSTTY_SCRIPT, [String(await ownerPid(tty)), command, ...args]);
|
|
116
|
+
}
|
|
117
|
+
let scale = null;
|
|
118
|
+
async function backingScale() {
|
|
119
|
+
if (scale !== null)
|
|
120
|
+
return scale;
|
|
121
|
+
const read = await run("osascript", [
|
|
122
|
+
"-l",
|
|
123
|
+
"JavaScript",
|
|
124
|
+
"-e",
|
|
125
|
+
"ObjC.import('AppKit'); $.NSScreen.mainScreen.backingScaleFactor",
|
|
126
|
+
]).catch(() => "");
|
|
127
|
+
const found = Number(read.trim());
|
|
128
|
+
scale = Number.isFinite(found) && found > 0 ? found : 2;
|
|
129
|
+
return scale;
|
|
130
|
+
}
|
|
131
|
+
async function listPanes(tty = null) {
|
|
132
|
+
const listed = [];
|
|
133
|
+
const pids = new Map();
|
|
134
|
+
directories.clear();
|
|
135
|
+
for (const line of (await ghosttyCommand("list", [], tty)).split("\n")) {
|
|
136
|
+
if (!line.trim())
|
|
137
|
+
continue;
|
|
138
|
+
const [window, tab, pane, pid, paneTty, ...directory] = line.split("\t");
|
|
139
|
+
directories.set(pane, directory.join("\t"));
|
|
140
|
+
if (pid && !paneTty)
|
|
141
|
+
pids.set(pane, pid);
|
|
142
|
+
listed.push({ id: pane, tab: `${window}:${tab}`, tty: paneTty || null, command: null });
|
|
143
|
+
}
|
|
144
|
+
if (pids.size > 0) {
|
|
145
|
+
const byPid = new Map();
|
|
146
|
+
const listing = await run("ps", ["-o", "pid=,tty=", "-p", [...pids.values()].join(",")]).catch(() => "");
|
|
147
|
+
for (const line of listing.split("\n")) {
|
|
148
|
+
const [pid, paneTty] = line.trim().split(/\s+/);
|
|
149
|
+
if (pid && paneTty && paneTty !== "??")
|
|
150
|
+
byPid.set(pid, `/dev/${paneTty}`);
|
|
151
|
+
}
|
|
152
|
+
for (const pane of listed) {
|
|
153
|
+
const pid = pids.get(pane.id);
|
|
154
|
+
if (pid)
|
|
155
|
+
pane.tty = byPid.get(pid) ?? null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return listed;
|
|
159
|
+
}
|
|
160
|
+
const paneByTty = new Map();
|
|
161
|
+
const missedAt = new Map();
|
|
162
|
+
const MISS_MEMORY_MS = 60_000;
|
|
163
|
+
async function cwdByPane() {
|
|
164
|
+
const cwds = new Map();
|
|
165
|
+
for (const line of (await ghosttyCommand("list", [])).split("\n")) {
|
|
166
|
+
if (!line.trim())
|
|
167
|
+
continue;
|
|
168
|
+
const [, , pane, , , ...directory] = line.split("\t");
|
|
169
|
+
cwds.set(pane, directory.join("\t"));
|
|
170
|
+
}
|
|
171
|
+
return cwds;
|
|
172
|
+
}
|
|
173
|
+
async function processCwd(pid) {
|
|
174
|
+
const listing = await run("lsof", ["-a", "-d", "cwd", "-Fn", "-p", pid]).catch(() => "");
|
|
175
|
+
const found = listing.split("\n").find((line) => line.startsWith("n/"));
|
|
176
|
+
return found ? found.slice(1) : node_os_1.default.homedir();
|
|
177
|
+
}
|
|
178
|
+
async function mapTtys(ttys, listed, cwdOf) {
|
|
179
|
+
const alive = new Set(listed.map((pane) => pane.id));
|
|
180
|
+
const pending = ttys.filter((tty) => {
|
|
181
|
+
const known = paneByTty.get(tty);
|
|
182
|
+
if (known && alive.has(known))
|
|
183
|
+
return false;
|
|
184
|
+
return Date.now() - (missedAt.get(tty) ?? 0) > MISS_MEMORY_MS;
|
|
185
|
+
});
|
|
186
|
+
if (pending.length === 0)
|
|
187
|
+
return;
|
|
188
|
+
const markers = new Map();
|
|
189
|
+
for (const tty of pending) {
|
|
190
|
+
const marker = markerDirectory();
|
|
191
|
+
try {
|
|
192
|
+
(0, shared_1.setPaneWorkingDirectory)(tty, marker);
|
|
193
|
+
markers.set(tty, marker);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
missedAt.set(tty, Date.now());
|
|
197
|
+
node_fs_1.default.rmSync(marker, { recursive: true, force: true });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const mapped = new Map();
|
|
201
|
+
try {
|
|
202
|
+
// Ghostty applies OSC 7 as soon as it reads it, so one listing normally sees every
|
|
203
|
+
// marker; a second look only happens when the first saw none at all
|
|
204
|
+
for (let attempt = 0; attempt < 2 && mapped.size === 0 && markers.size > 0; attempt++) {
|
|
205
|
+
if (attempt > 0)
|
|
206
|
+
await (0, shared_1.sleep)(120);
|
|
207
|
+
for (const [pane, cwd] of await cwdByPane()) {
|
|
208
|
+
for (const [tty, marker] of markers) {
|
|
209
|
+
if (cwd === marker && alive.has(pane))
|
|
210
|
+
mapped.set(tty, pane);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
for (const [tty, marker] of markers) {
|
|
217
|
+
const pane = mapped.get(tty);
|
|
218
|
+
let restore = node_os_1.default.homedir();
|
|
219
|
+
if (pane) {
|
|
220
|
+
paneByTty.set(tty, pane);
|
|
221
|
+
restore = directories.get(pane) ?? restore;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
missedAt.set(tty, Date.now());
|
|
225
|
+
restore = await cwdOf(tty).catch(() => restore);
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
(0, shared_1.setPaneWorkingDirectory)(tty, restore);
|
|
229
|
+
}
|
|
230
|
+
catch { }
|
|
231
|
+
node_fs_1.default.rmSync(marker, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function ttysRunning(match) {
|
|
236
|
+
const byTty = new Map();
|
|
237
|
+
const listing = await run("ps", ["-e", "-o", "pid=,tty=,args="]).catch(() => "");
|
|
238
|
+
for (const line of listing.split("\n")) {
|
|
239
|
+
const parts = line.trim().match(/^(\d+)\s+(\S+)\s+(.*)$/);
|
|
240
|
+
if (!parts || parts[2].startsWith("?"))
|
|
241
|
+
continue;
|
|
242
|
+
const tty = `/dev/${parts[2]}`;
|
|
243
|
+
byTty.set(tty, [...(byTty.get(tty) ?? []), { pid: parts[1], command: parts[3] }]);
|
|
244
|
+
}
|
|
245
|
+
const matched = new Map();
|
|
246
|
+
for (const [tty, processes] of byTty) {
|
|
247
|
+
const command = processes.map((running) => running.command).join("\n");
|
|
248
|
+
if (match(command))
|
|
249
|
+
matched.set(tty, { pid: processes[processes.length - 1].pid, command });
|
|
250
|
+
}
|
|
251
|
+
return matched;
|
|
252
|
+
}
|
|
253
|
+
async function listPanesMatching(options) {
|
|
254
|
+
const listed = await listPanes(options?.tty ?? null);
|
|
255
|
+
if (!options?.commands || listed.some((pane) => pane.tty))
|
|
256
|
+
return listed;
|
|
257
|
+
const running = await ttysRunning(options.commands);
|
|
258
|
+
await mapTtys([...running.keys()], listed, (tty) => processCwd(running.get(tty).pid));
|
|
259
|
+
for (const [tty, { command }] of running) {
|
|
260
|
+
const pane = listed.find((candidate) => candidate.id === paneByTty.get(tty));
|
|
261
|
+
if (!pane)
|
|
262
|
+
continue;
|
|
263
|
+
pane.tty = tty;
|
|
264
|
+
pane.command = command;
|
|
265
|
+
}
|
|
266
|
+
return listed;
|
|
267
|
+
}
|
|
268
|
+
async function getCurrentPane({ tty, cwd }) {
|
|
269
|
+
if (!tty)
|
|
270
|
+
return null;
|
|
271
|
+
const before = await listPanes(tty);
|
|
272
|
+
const byTty = before.find((pane) => pane.tty === tty);
|
|
273
|
+
if (byTty)
|
|
274
|
+
return byTty;
|
|
275
|
+
missedAt.delete(tty);
|
|
276
|
+
await mapTtys([tty], before, async () => cwd);
|
|
277
|
+
const id = paneByTty.get(tty);
|
|
278
|
+
const found = id ? before.find((pane) => pane.id === id) : null;
|
|
279
|
+
if (!found) {
|
|
280
|
+
throw new Error(`could not find this pane in Ghostty — we marked ${tty} and no Ghostty pane reported it back, so ${tty} is not a Ghostty pane (a shell inside tmux or a remote session looks like this)`);
|
|
281
|
+
}
|
|
282
|
+
return found;
|
|
283
|
+
}
|
|
284
|
+
async function sizeSplit(pane, direction, size) {
|
|
285
|
+
const pixels = await (0, graphics_1.panePixels)();
|
|
286
|
+
if (!pixels)
|
|
287
|
+
return;
|
|
288
|
+
const sideways = direction === "right" || direction === "left";
|
|
289
|
+
const whole = sideways ? pixels.width : pixels.height;
|
|
290
|
+
const points = ((size - 0.5) * whole) / (await backingScale());
|
|
291
|
+
const amount = Math.round(Math.abs(points));
|
|
292
|
+
if (amount < 3)
|
|
293
|
+
return;
|
|
294
|
+
const away = {
|
|
295
|
+
right: "left",
|
|
296
|
+
left: "right",
|
|
297
|
+
down: "up",
|
|
298
|
+
up: "down",
|
|
299
|
+
};
|
|
300
|
+
const grow = points >= 0 ? away[direction] : direction;
|
|
301
|
+
await ghosttyCommand("action", [pane, `resize_split:${grow},${amount}`]);
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
name: "ghostty",
|
|
305
|
+
getCurrentPane,
|
|
306
|
+
listPanes: listPanesMatching,
|
|
307
|
+
async sendText(pane, text) {
|
|
308
|
+
const result = await ghosttyCommand("input", [pane, text]);
|
|
309
|
+
if (result !== "ok")
|
|
310
|
+
throw new Error(`Ghostty could not type into pane ${pane}`);
|
|
311
|
+
},
|
|
312
|
+
async neighbor(from, direction) {
|
|
313
|
+
const [window, tab] = from.tab.split(":");
|
|
314
|
+
const result = (await ghosttyCommand("neighbor", [window, tab, from.id, direction])).trim();
|
|
315
|
+
return result && result !== "not-found" ? { id: result, tab: from.tab } : null;
|
|
316
|
+
},
|
|
317
|
+
async focusPane(pane) {
|
|
318
|
+
const result = await ghosttyCommand("focus", [pane]);
|
|
319
|
+
if (result !== "ok")
|
|
320
|
+
throw new Error(`Ghostty could not focus pane ${pane}`);
|
|
321
|
+
},
|
|
322
|
+
async split({ from, direction, command, size }) {
|
|
323
|
+
const startDir = directories.get(from.id) ?? process.cwd();
|
|
324
|
+
// Ghostty execs the command from login, and Electron logs a code-signing
|
|
325
|
+
// error when its parent is login; a shell in between keeps the pane quiet
|
|
326
|
+
const wrapped = (0, shared_1.shellQuote)(["/bin/sh", "-c", `${(0, shared_1.shellQuote)(command)}; exit $?`]);
|
|
327
|
+
const opened = await ghosttyCommand("split", [from.id, DIRECTION_CODES[direction], startDir, wrapped]);
|
|
328
|
+
if (!opened || opened === "not-found") {
|
|
329
|
+
throw new Error("this pane went away before we could split it");
|
|
330
|
+
}
|
|
331
|
+
if (size !== null)
|
|
332
|
+
await sizeSplit(opened, direction, size);
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
exports.ghostty = ghostty;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.herdr = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const shared_1 = require("../shared");
|
|
12
|
+
function herdrConfigPath(env) {
|
|
13
|
+
if (env.HERDR_CONFIG_PATH)
|
|
14
|
+
return env.HERDR_CONFIG_PATH;
|
|
15
|
+
if (process.platform === "win32") {
|
|
16
|
+
return node_path_1.default.join(env.APPDATA ?? node_path_1.default.join(env.HOME ?? node_os_1.default.homedir(), "AppData", "Roaming"), "herdr", "config.toml");
|
|
17
|
+
}
|
|
18
|
+
return node_path_1.default.join(env.HOME ?? node_os_1.default.homedir(), ".config", "herdr", "config.toml");
|
|
19
|
+
}
|
|
20
|
+
function enableKittyGraphics(config) {
|
|
21
|
+
const flag = /^([ \t]*kitty_graphics[ \t]*=[ \t]*)false([ \t]*)$/m;
|
|
22
|
+
if (flag.test(config))
|
|
23
|
+
return config.replace(flag, "$1true$2");
|
|
24
|
+
const header = /^\[experimental\][ \t]*$/m;
|
|
25
|
+
if (header.test(config))
|
|
26
|
+
return config.replace(header, "[experimental]\nkitty_graphics = true");
|
|
27
|
+
const trimmed = config.replace(/\s+$/, "");
|
|
28
|
+
return `${trimmed}${trimmed ? "\n\n" : ""}[experimental]\nkitty_graphics = true\n`;
|
|
29
|
+
}
|
|
30
|
+
const NATIVE_DIRECTION = {
|
|
31
|
+
right: "right",
|
|
32
|
+
left: "right",
|
|
33
|
+
down: "down",
|
|
34
|
+
up: "down",
|
|
35
|
+
};
|
|
36
|
+
const OPPOSITE = { right: "left", down: "up" };
|
|
37
|
+
const herdr = (env, run) => {
|
|
38
|
+
if (!env.HERDR_PANE_ID)
|
|
39
|
+
return null;
|
|
40
|
+
const bin = env.HERDR_BIN_PATH || "herdr";
|
|
41
|
+
const herdr = (args) => run(bin, args);
|
|
42
|
+
async function splitPane(args) {
|
|
43
|
+
return JSON.parse(await herdr(["pane", "split", ...args, "--right-click", "pane"]));
|
|
44
|
+
}
|
|
45
|
+
async function prepare() {
|
|
46
|
+
try {
|
|
47
|
+
const configPath = herdrConfigPath(env);
|
|
48
|
+
const current = node_fs_1.default.existsSync(configPath) ? node_fs_1.default.readFileSync(configPath, "utf8") : "";
|
|
49
|
+
if (/^[ \t]*kitty_graphics[ \t]*=[ \t]*true[ \t]*$/m.test(current))
|
|
50
|
+
return;
|
|
51
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(configPath), { recursive: true });
|
|
52
|
+
node_fs_1.default.writeFileSync(configPath, enableKittyGraphics(current));
|
|
53
|
+
await herdr(["server", "reload-config"]);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function listPanes() {
|
|
59
|
+
const { result } = JSON.parse(await herdr(["pane", "list"]));
|
|
60
|
+
const workspace = env.HERDR_PANE_ID.split(":")[0];
|
|
61
|
+
const visible = result.panes.filter((pane) => (pane.workspace_id ?? pane.pane_id.split(":")[0]) === workspace);
|
|
62
|
+
const processes = await Promise.all(visible.map((pane) => herdr(["pane", "process-info", "--pane", pane.pane_id])
|
|
63
|
+
.then((out) => JSON.parse(out).result.process_info)
|
|
64
|
+
.catch(() => ({}))));
|
|
65
|
+
const ttyByPid = await shellTtys(processes.map((info) => info.shell_pid).filter((pid) => pid != null));
|
|
66
|
+
return visible.map((pane, i) => ({
|
|
67
|
+
id: pane.pane_id,
|
|
68
|
+
tab: pane.tab_id,
|
|
69
|
+
tty: processes[i].shell_pid != null ? ttyByPid.get(processes[i].shell_pid) ?? null : null,
|
|
70
|
+
command: (processes[i].foreground_processes ?? []).map((process) => process.cmdline ?? "").join("\n") || null,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
async function shellTtys(pids) {
|
|
74
|
+
const byPid = new Map();
|
|
75
|
+
if (pids.length === 0)
|
|
76
|
+
return byPid;
|
|
77
|
+
const listing = await run("ps", ["-o", "pid=,tty=", "-p", pids.join(",")]).catch(() => "");
|
|
78
|
+
for (const line of listing.split("\n")) {
|
|
79
|
+
const [pid, tty] = line.trim().split(/\s+/);
|
|
80
|
+
if (pid && tty && tty !== "??")
|
|
81
|
+
byPid.set(Number(pid), `/dev/${tty}`);
|
|
82
|
+
}
|
|
83
|
+
return byPid;
|
|
84
|
+
}
|
|
85
|
+
function socketCall(method, params) {
|
|
86
|
+
const socketPath = env.HERDR_SOCKET_PATH ?? node_path_1.default.join(env.HOME ?? node_os_1.default.homedir(), ".config", "herdr", "herdr.sock");
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const socket = node_net_1.default.connect(socketPath);
|
|
89
|
+
let buffer = "";
|
|
90
|
+
socket.setTimeout(5000, () => socket.destroy(new Error("herdr socket timed out")));
|
|
91
|
+
socket.on("error", reject);
|
|
92
|
+
socket.on("data", (chunk) => {
|
|
93
|
+
buffer += chunk.toString();
|
|
94
|
+
const line = buffer.indexOf("\n");
|
|
95
|
+
if (line < 0)
|
|
96
|
+
return;
|
|
97
|
+
socket.end();
|
|
98
|
+
const reply = JSON.parse(buffer.slice(0, line));
|
|
99
|
+
if (reply.error)
|
|
100
|
+
reject(new Error(reply.error.message ?? "herdr socket call failed"));
|
|
101
|
+
else
|
|
102
|
+
resolve(reply.result);
|
|
103
|
+
});
|
|
104
|
+
socket.on("connect", () => {
|
|
105
|
+
socket.write(`${JSON.stringify({ id: "pixel", method, params })}\n`);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function focusPane(pane) {
|
|
110
|
+
const { result } = JSON.parse(await herdr(["pane", "get", pane]));
|
|
111
|
+
if (result.pane.tab_id !== env.HERDR_TAB_ID)
|
|
112
|
+
await herdr(["tab", "focus", result.pane.tab_id]);
|
|
113
|
+
await socketCall("pane.focus", { pane_id: pane });
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
name: "herdr",
|
|
117
|
+
prepare,
|
|
118
|
+
getCurrentPane: async () => ({ id: env.HERDR_PANE_ID, tab: env.HERDR_TAB_ID }),
|
|
119
|
+
listPanes,
|
|
120
|
+
async neighbor(from, direction) {
|
|
121
|
+
const { result } = JSON.parse(await herdr(["pane", "neighbor", "--pane", from.id, "--direction", direction]));
|
|
122
|
+
const id = result.neighbor.neighbor_pane_id;
|
|
123
|
+
return id ? { id, tab: result.neighbor.layout.tab_id } : null;
|
|
124
|
+
},
|
|
125
|
+
async sendText(pane, text) {
|
|
126
|
+
await herdr(["pane", "send-text", pane, (0, shared_1.bracketedPaste)(text)]);
|
|
127
|
+
},
|
|
128
|
+
focusPane,
|
|
129
|
+
async split({ from, direction, command, size }) {
|
|
130
|
+
const native = NATIVE_DIRECTION[direction];
|
|
131
|
+
const ratio = size ? ["--ratio", String(size)] : [];
|
|
132
|
+
const { result } = await splitPane(["--pane", from.id, "--direction", native, "--focus", ...ratio]);
|
|
133
|
+
const newPaneId = result.pane.pane_id;
|
|
134
|
+
if (direction === "left" || direction === "up") {
|
|
135
|
+
await herdr(["pane", "swap", "--pane", newPaneId, "--direction", OPPOSITE[native]]);
|
|
136
|
+
}
|
|
137
|
+
await herdr(["pane", "run", newPaneId, (0, shared_1.shellQuote)(command)]);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
exports.herdr = herdr;
|