herdr-remote 0.2.18 → 0.2.19
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/bin/herdr-remote.js +3 -225
- package/dist/agent-status.js +130 -0
- package/dist/binary-plist.js +157 -0
- package/dist/cli.js +255 -0
- package/dist/config.js +270 -0
- package/dist/connector/agent-watch.js +110 -0
- package/dist/connector/font-server.js +165 -0
- package/dist/connector/host-connector.js +430 -0
- package/dist/connector/lock.js +66 -0
- package/dist/connector/main.js +18 -0
- package/dist/connector/sessions.js +366 -0
- package/dist/connector/update-report.js +67 -0
- package/dist/connector/wire.js +42 -0
- package/{src → dist}/exit-codes.js +1 -4
- package/dist/font-subset.js +110 -0
- package/dist/herdr-api.js +246 -0
- package/dist/herdr-command.js +223 -0
- package/dist/herdr-plugin.js +116 -0
- package/dist/herdr-server.js +189 -0
- package/dist/i18n/en.js +223 -0
- package/dist/i18n/index.js +48 -0
- package/dist/i18n/zh.js +222 -0
- package/dist/keepalive/detached.js +78 -0
- package/dist/keepalive/environment.js +44 -0
- package/dist/keepalive/index.js +110 -0
- package/dist/keepalive/launchd.js +117 -0
- package/dist/keepalive/systemd.js +108 -0
- package/dist/keepalive/types.js +1 -0
- package/dist/lib/process.js +12 -0
- package/dist/lifecycle.js +59 -0
- package/dist/net-interfaces.js +73 -0
- package/dist/pasted-files.js +115 -0
- package/dist/paths.js +62 -0
- package/dist/pty-session.js +96 -0
- package/dist/relay-client.js +106 -0
- package/dist/relay-urls.js +99 -0
- package/dist/runtime.js +106 -0
- package/dist/service.js +260 -0
- package/dist/settings-model.js +298 -0
- package/dist/socket-discovery.js +48 -0
- package/dist/supervisor.js +319 -0
- package/dist/terminal-font/deps.js +73 -0
- package/dist/terminal-font/files.js +354 -0
- package/dist/terminal-font/formats.js +335 -0
- package/dist/terminal-font/index.js +187 -0
- package/dist/terminal-font/readers.js +316 -0
- package/dist/terminal-font/terminals.js +128 -0
- package/dist/terminal-palette.js +339 -0
- package/dist/tui/App.js +195 -0
- package/dist/tui/api.js +79 -0
- package/dist/tui/components/TextField.js +50 -0
- package/dist/tui/components/common.js +80 -0
- package/dist/tui/index.js +36 -0
- package/dist/tui/mouse/index.js +164 -0
- package/dist/tui/mouse/protocol.js +96 -0
- package/dist/tui/mouse/source.js +74 -0
- package/dist/tui/screens/About.js +166 -0
- package/dist/tui/screens/Herdr.js +74 -0
- package/dist/tui/screens/Keepalive.js +84 -0
- package/dist/tui/screens/Overview.js +36 -0
- package/dist/tui/screens/Pair.js +74 -0
- package/dist/tui/screens/Relay.js +242 -0
- package/dist/tui/screens/Services.js +63 -0
- package/dist/tui/screens/Wizard.js +235 -0
- package/dist/tui/terminal.js +39 -0
- package/dist/tui/theme.js +33 -0
- package/dist/updater.js +382 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +8 -8
- package/dist/tui.mjs +0 -5775
- package/src/agent-status.js +0 -139
- package/src/binary-plist.js +0 -146
- package/src/config.js +0 -454
- package/src/font-subset.js +0 -110
- package/src/herdr-api.js +0 -244
- package/src/herdr-command.js +0 -231
- package/src/herdr-plugin.js +0 -130
- package/src/herdr-server.js +0 -201
- package/src/host-connector.js +0 -1069
- package/src/i18n/en.js +0 -240
- package/src/i18n/index.js +0 -56
- package/src/i18n/zh.js +0 -239
- package/src/keepalive.js +0 -460
- package/src/lifecycle.js +0 -62
- package/src/net-interfaces.js +0 -76
- package/src/pasted-files.js +0 -176
- package/src/pty-session.js +0 -92
- package/src/service.js +0 -530
- package/src/settings-model.js +0 -323
- package/src/socket-discovery.js +0 -60
- package/src/state.js +0 -47
- package/src/supervisor.js +0 -250
- package/src/terminal-font.js +0 -1204
- package/src/terminal-palette.js +0 -325
- package/src/updater.js +0 -394
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// Each terminal's own settings, read the way that terminal reads them.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseBinaryPlist, unarchiveKeyed } from '../binary-plist.js';
|
|
4
|
+
import { configHome, dataHome, expandHome, pointsToPx, positiveNumber, } from './deps.js';
|
|
5
|
+
import { familyForPostScriptName } from './files.js';
|
|
6
|
+
import { firstCssFamily, optionValue, parseFontconfigPattern, parseGVariantString, parseIni, parseJsonc, parsePangoFontDescription, parseQtFontString, parseTomlSubset, parseXFontName, xResource, xrmResources, } from './formats.js';
|
|
7
|
+
function gsettingsGet(deps, schema, key) {
|
|
8
|
+
const result = deps.run('gsettings', ['get', schema, key]);
|
|
9
|
+
if (result.status !== 0)
|
|
10
|
+
return null;
|
|
11
|
+
return result.stdout.trim();
|
|
12
|
+
}
|
|
13
|
+
/** A GNOME-style profile: its own font, or the desktop's monospace font. */
|
|
14
|
+
function readGSettingsProfileFont(deps, { schema, path: profilePath, fontKey = 'font', }) {
|
|
15
|
+
const useSystem = gsettingsGet(deps, `${schema}:${profilePath}`, 'use-system-font');
|
|
16
|
+
if (useSystem === null)
|
|
17
|
+
return null;
|
|
18
|
+
if (useSystem === 'false') {
|
|
19
|
+
const font = parsePangoFontDescription(parseGVariantString(gsettingsGet(deps, `${schema}:${profilePath}`, fontKey)));
|
|
20
|
+
if (font)
|
|
21
|
+
return font;
|
|
22
|
+
}
|
|
23
|
+
return systemMonospaceFont(deps);
|
|
24
|
+
}
|
|
25
|
+
function systemMonospaceFont(deps) {
|
|
26
|
+
return parsePangoFontDescription(parseGVariantString(gsettingsGet(deps, 'org.gnome.desktop.interface', 'monospace-font-name')));
|
|
27
|
+
}
|
|
28
|
+
export const READERS = {
|
|
29
|
+
'gnome-terminal': (deps) => {
|
|
30
|
+
const id = parseGVariantString(gsettingsGet(deps, 'org.gnome.Terminal.ProfilesList', 'default'));
|
|
31
|
+
if (!id)
|
|
32
|
+
return null;
|
|
33
|
+
return readGSettingsProfileFont(deps, {
|
|
34
|
+
schema: 'org.gnome.Terminal.Legacy.Profile',
|
|
35
|
+
path: `/org/gnome/terminal/legacy/profiles:/:${id}/`,
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
tilix: (deps) => {
|
|
39
|
+
const id = parseGVariantString(gsettingsGet(deps, 'com.gexperts.Tilix.ProfilesList', 'default'));
|
|
40
|
+
if (!id)
|
|
41
|
+
return null;
|
|
42
|
+
return readGSettingsProfileFont(deps, {
|
|
43
|
+
schema: 'com.gexperts.Tilix.Profile',
|
|
44
|
+
path: `/com/gexperts/Tilix/profiles/${id}/`,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
ptyxis: (deps) => {
|
|
48
|
+
const useSystem = gsettingsGet(deps, 'org.gnome.Ptyxis', 'use-system-font');
|
|
49
|
+
if (useSystem === 'false') {
|
|
50
|
+
const font = parsePangoFontDescription(parseGVariantString(gsettingsGet(deps, 'org.gnome.Ptyxis', 'font-name')));
|
|
51
|
+
if (font)
|
|
52
|
+
return font;
|
|
53
|
+
}
|
|
54
|
+
return useSystem === null ? null : systemMonospaceFont(deps);
|
|
55
|
+
},
|
|
56
|
+
konsole: (deps) => {
|
|
57
|
+
const rc = parseIni(deps.readFile(path.join(configHome(deps), 'konsolerc')));
|
|
58
|
+
const profileName = rc['Desktop Entry']?.DefaultProfile;
|
|
59
|
+
if (profileName) {
|
|
60
|
+
const profile = parseIni(deps.readFile(path.join(dataHome(deps), 'konsole', profileName)));
|
|
61
|
+
const font = parseQtFontString(profile.Appearance?.Font);
|
|
62
|
+
if (font)
|
|
63
|
+
return font;
|
|
64
|
+
}
|
|
65
|
+
// Konsole's built-in profile draws with the desktop's fixed-width font.
|
|
66
|
+
const globals = parseIni(deps.readFile(path.join(configHome(deps), 'kdeglobals')));
|
|
67
|
+
return parseQtFontString(globals.General?.fixed) || { family: 'Hack', sizePx: pointsToPx(10) };
|
|
68
|
+
},
|
|
69
|
+
'xfce4-terminal': (deps) => {
|
|
70
|
+
// 1.1+ keeps its settings in xfconf; older releases in terminalrc.
|
|
71
|
+
const query = (property) => {
|
|
72
|
+
const result = deps.run('xfconf-query', ['-c', 'xfce4-terminal', '-p', property]);
|
|
73
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
74
|
+
};
|
|
75
|
+
const rc = parseIni(deps.readFile(path.join(configHome(deps), 'xfce4', 'terminal', 'terminalrc')))
|
|
76
|
+
.Configuration || {};
|
|
77
|
+
const useSystem = (query('/font-use-system') ?? rc.FontUseSystem ?? 'false').toLowerCase() === 'true';
|
|
78
|
+
if (!useSystem) {
|
|
79
|
+
const font = parsePangoFontDescription(query('/font-name') ?? rc.FontName ?? 'Monospace 12');
|
|
80
|
+
if (font)
|
|
81
|
+
return font;
|
|
82
|
+
}
|
|
83
|
+
const result = deps.run('xfconf-query', ['-c', 'xsettings', '-p', '/Gtk/MonospaceFontName']);
|
|
84
|
+
return parsePangoFontDescription(result.status === 0 ? result.stdout.trim() : 'Monospace 10');
|
|
85
|
+
},
|
|
86
|
+
kitty: (deps) => {
|
|
87
|
+
const directory = deps.env.KITTY_CONFIG_DIRECTORY || path.join(configHome(deps), 'kitty');
|
|
88
|
+
const settings = {};
|
|
89
|
+
const visit = (file, depth) => {
|
|
90
|
+
const text = deps.readFile(file);
|
|
91
|
+
if (text === null || depth > 4)
|
|
92
|
+
return;
|
|
93
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
94
|
+
const line = rawLine.trim();
|
|
95
|
+
if (!line || line.startsWith('#'))
|
|
96
|
+
continue;
|
|
97
|
+
const [key, ...rest] = line.split(/\s+/);
|
|
98
|
+
const value = rest.join(' ');
|
|
99
|
+
if (key === 'include')
|
|
100
|
+
visit(path.resolve(directory, expandHome(value, deps)), depth + 1);
|
|
101
|
+
else if (key === 'font_family' || key === 'font_size')
|
|
102
|
+
settings[key] = value;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
visit(path.join(directory, 'kitty.conf'), 0);
|
|
106
|
+
// kitty 0.33+ also accepts `font_family family="JetBrains Mono" style=...`.
|
|
107
|
+
const raw = settings.font_family || 'monospace';
|
|
108
|
+
const quoted = /family\s*=\s*(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(raw);
|
|
109
|
+
const family = quoted ? quoted[1] || quoted[2] || quoted[3] : raw;
|
|
110
|
+
return { family, sizePx: pointsToPx(settings.font_size || 11) };
|
|
111
|
+
},
|
|
112
|
+
alacritty: (deps) => {
|
|
113
|
+
const candidates = [
|
|
114
|
+
path.join(configHome(deps), 'alacritty', 'alacritty.toml'),
|
|
115
|
+
path.join(configHome(deps), 'alacritty.toml'),
|
|
116
|
+
path.join(deps.home, '.alacritty.toml'),
|
|
117
|
+
];
|
|
118
|
+
const merged = {};
|
|
119
|
+
const visit = (file, depth) => {
|
|
120
|
+
const text = deps.readFile(file);
|
|
121
|
+
if (text === null || depth > 4)
|
|
122
|
+
return false;
|
|
123
|
+
const values = parseTomlSubset(text);
|
|
124
|
+
// Imports come first; the importing file overrides them.
|
|
125
|
+
for (const imported of [
|
|
126
|
+
...(values.import || []),
|
|
127
|
+
...(values['general.import'] || []),
|
|
128
|
+
]) {
|
|
129
|
+
visit(path.resolve(path.dirname(file), expandHome(String(imported), deps)), depth + 1);
|
|
130
|
+
}
|
|
131
|
+
Object.assign(merged, values);
|
|
132
|
+
return true;
|
|
133
|
+
};
|
|
134
|
+
candidates.some((file) => visit(file, 0));
|
|
135
|
+
const fallbackFamily = deps.platform === 'darwin' ? 'Menlo' : 'monospace';
|
|
136
|
+
return {
|
|
137
|
+
family: String(merged['font.normal.family'] || fallbackFamily),
|
|
138
|
+
sizePx: pointsToPx(merged['font.size'] || 11.25),
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
ghostty: (deps) => {
|
|
142
|
+
const settings = {
|
|
143
|
+
families: [],
|
|
144
|
+
size: undefined,
|
|
145
|
+
};
|
|
146
|
+
const visit = (file, depth) => {
|
|
147
|
+
const text = deps.readFile(file);
|
|
148
|
+
if (text === null || depth > 4)
|
|
149
|
+
return;
|
|
150
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
151
|
+
const line = rawLine.trim();
|
|
152
|
+
if (!line || line.startsWith('#'))
|
|
153
|
+
continue;
|
|
154
|
+
const equals = line.indexOf('=');
|
|
155
|
+
if (equals === -1)
|
|
156
|
+
continue;
|
|
157
|
+
const key = line.slice(0, equals).trim();
|
|
158
|
+
const value = line
|
|
159
|
+
.slice(equals + 1)
|
|
160
|
+
.trim()
|
|
161
|
+
.replace(/^"(.*)"$/, '$1');
|
|
162
|
+
if (key === 'font-family') {
|
|
163
|
+
// An empty value resets the list, per Ghostty's repeatable keys.
|
|
164
|
+
if (value)
|
|
165
|
+
settings.families.push(value);
|
|
166
|
+
else
|
|
167
|
+
settings.families = [];
|
|
168
|
+
}
|
|
169
|
+
else if (key === 'font-size') {
|
|
170
|
+
settings.size = value;
|
|
171
|
+
}
|
|
172
|
+
else if (key === 'config-file') {
|
|
173
|
+
const target = expandHome(value.replace(/^\?/, ''), deps);
|
|
174
|
+
visit(path.resolve(path.dirname(file), target), depth + 1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
visit(path.join(configHome(deps), 'ghostty', 'config'), 0);
|
|
179
|
+
if (deps.platform === 'darwin') {
|
|
180
|
+
visit(path.join(deps.home, 'Library', 'Application Support', 'com.mitchellh.ghostty', 'config'), 0);
|
|
181
|
+
}
|
|
182
|
+
// Ghostty ships JetBrains Mono inside the binary and draws with it by default.
|
|
183
|
+
return {
|
|
184
|
+
family: settings.families[0] || 'JetBrains Mono',
|
|
185
|
+
sizePx: pointsToPx(settings.size || (deps.platform === 'darwin' ? 13 : 12)),
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
wezterm: (deps) => {
|
|
189
|
+
const result = deps.run('wezterm', ['ls-fonts']);
|
|
190
|
+
const listing = result.status === 0 ? result.stdout : '';
|
|
191
|
+
const match = /family\s*=\s*"([^"]+)"/.exec(listing) ||
|
|
192
|
+
/font_with_fallback\(\{[\s\S]*?"([^"]+)"/.exec(listing);
|
|
193
|
+
let sizePt = 12;
|
|
194
|
+
for (const file of [
|
|
195
|
+
path.join(deps.home, '.wezterm.lua'),
|
|
196
|
+
path.join(configHome(deps), 'wezterm', 'wezterm.lua'),
|
|
197
|
+
]) {
|
|
198
|
+
const size = /font_size\s*=\s*(\d+(?:\.\d+)?)/.exec(deps.readFile(file) || '');
|
|
199
|
+
if (size) {
|
|
200
|
+
sizePt = Number(size[1]);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// WezTerm, like Ghostty, embeds JetBrains Mono as its default.
|
|
205
|
+
return { family: match ? match[1] : 'JetBrains Mono', sizePx: pointsToPx(sizePt) };
|
|
206
|
+
},
|
|
207
|
+
foot: (deps) => {
|
|
208
|
+
const ini = parseIni(deps.readFile(path.join(configHome(deps), 'foot', 'foot.ini')));
|
|
209
|
+
const value = ini.main?.font || ini['']?.font || 'monospace:size=8';
|
|
210
|
+
return parseFontconfigPattern(value);
|
|
211
|
+
},
|
|
212
|
+
vscode: (deps) => {
|
|
213
|
+
const base = deps.platform === 'darwin'
|
|
214
|
+
? path.join(deps.home, 'Library', 'Application Support')
|
|
215
|
+
: configHome(deps);
|
|
216
|
+
for (const product of ['Code', 'Cursor', 'Code - Insiders', 'VSCodium', 'Windsurf']) {
|
|
217
|
+
const settings = parseJsonc(deps.readFile(path.join(base, product, 'User', 'settings.json')));
|
|
218
|
+
if (!settings)
|
|
219
|
+
continue;
|
|
220
|
+
const family = firstCssFamily(settings['terminal.integrated.fontFamily']) ||
|
|
221
|
+
firstCssFamily(settings['editor.fontFamily']);
|
|
222
|
+
const sizePx = positiveNumber(settings['terminal.integrated.fontSize']) ||
|
|
223
|
+
positiveNumber(settings['editor.fontSize']) ||
|
|
224
|
+
14;
|
|
225
|
+
if (family)
|
|
226
|
+
return { family, sizePx };
|
|
227
|
+
}
|
|
228
|
+
return { family: deps.platform === 'darwin' ? 'Menlo' : 'Droid Sans Mono', sizePx: 14 };
|
|
229
|
+
},
|
|
230
|
+
iterm2: (deps) => {
|
|
231
|
+
const prefs = readPreferences('com.googlecode.iterm2', deps);
|
|
232
|
+
const bookmarks = prefs?.['New Bookmarks'];
|
|
233
|
+
const profiles = (Array.isArray(bookmarks) ? bookmarks : []);
|
|
234
|
+
const profile = profiles.find((candidate) => candidate?.Guid === prefs?.['Default Bookmark Guid']) ||
|
|
235
|
+
profiles[0];
|
|
236
|
+
// `JetBrainsMonoNF-Regular 13`: a PostScript name, then the size in points.
|
|
237
|
+
const match = /^(.+?)\s+(\d+(?:\.\d+)?)$/.exec(String(profile?.['Normal Font'] || '').trim());
|
|
238
|
+
if (!match)
|
|
239
|
+
return null;
|
|
240
|
+
const family = familyForPostScriptName(match[1], deps);
|
|
241
|
+
return family ? { family, sizePx: pointsToPx(match[2]) } : null;
|
|
242
|
+
},
|
|
243
|
+
'apple-terminal': (deps) => {
|
|
244
|
+
const prefs = readPreferences('com.apple.Terminal', deps);
|
|
245
|
+
if (!prefs)
|
|
246
|
+
return null;
|
|
247
|
+
const name = prefs['Default Window Settings'] || prefs['Startup Window Settings'] || 'Basic';
|
|
248
|
+
const profile = prefs['Window Settings']?.[String(name)];
|
|
249
|
+
// A profile that was never given a font draws with Terminal's own default,
|
|
250
|
+
// SF Mono 11 since macOS 10.15.
|
|
251
|
+
let postscript = 'SFMono-Regular';
|
|
252
|
+
let points = 11;
|
|
253
|
+
const archived = profile?.Font;
|
|
254
|
+
if (Buffer.isBuffer(archived)) {
|
|
255
|
+
// An archived NSFont: its PostScript name and point size.
|
|
256
|
+
const font = unarchiveKeyed(parseBinaryPlist(archived));
|
|
257
|
+
if (typeof font?.NSName === 'string' && font.NSName)
|
|
258
|
+
postscript = font.NSName;
|
|
259
|
+
if (Number(font?.NSSize) > 0)
|
|
260
|
+
points = Number(font?.NSSize);
|
|
261
|
+
}
|
|
262
|
+
const family = familyForPostScriptName(postscript, deps);
|
|
263
|
+
return family ? { family, sizePx: pointsToPx(points) } : null;
|
|
264
|
+
},
|
|
265
|
+
xterm: (deps) => {
|
|
266
|
+
// The emulator's own command line (`-fa`, `-fs`, `-xrm`) over the X
|
|
267
|
+
// resources, the way xterm itself ranks them. Without a faceName xterm
|
|
268
|
+
// draws with a bitmap core font, which no browser can load: no answer.
|
|
269
|
+
const classes = ['XTerm', 'xterm', 'UXTerm', 'uxterm'];
|
|
270
|
+
const args = deps.terminalArgs || [];
|
|
271
|
+
const resources = [...xResources(deps), ...xrmResources(args)];
|
|
272
|
+
const face = optionValue(args, ['-fa', '-faceName']) || xResource(resources, classes, 'faceName');
|
|
273
|
+
if (!face)
|
|
274
|
+
return null;
|
|
275
|
+
const font = parseXFontName(face, { bareIsPattern: true });
|
|
276
|
+
if (!font)
|
|
277
|
+
return null;
|
|
278
|
+
const size = Number(optionValue(args, ['-fs', '-faceSize']) || xResource(resources, classes, 'faceSize'));
|
|
279
|
+
return size > 0 ? { ...font, sizePx: pointsToPx(size) } : font;
|
|
280
|
+
},
|
|
281
|
+
urxvt: (deps) => {
|
|
282
|
+
const args = deps.terminalArgs || [];
|
|
283
|
+
const resources = [...xResources(deps), ...xrmResources(args)];
|
|
284
|
+
const font = optionValue(args, ['-fn']) ||
|
|
285
|
+
xResource(resources, ['URxvt', 'urxvt', 'Rxvt', 'rxvt'], 'font');
|
|
286
|
+
return font ? parseXFontName(font) : null;
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* A macOS preferences file, parsed. These are binary plists; one that is not
|
|
291
|
+
* (edited by hand) is left alone rather than guessed at.
|
|
292
|
+
*/
|
|
293
|
+
function readPreferences(domain, deps) {
|
|
294
|
+
const data = deps.readBuffer(path.join(deps.home, 'Library', 'Preferences', `${domain}.plist`));
|
|
295
|
+
if (!data)
|
|
296
|
+
return null;
|
|
297
|
+
try {
|
|
298
|
+
return parseBinaryPlist(data);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/** The X resource database, as `xrdb -query` prints it: `name:\tvalue`. */
|
|
305
|
+
function xResources(deps) {
|
|
306
|
+
const result = deps.run('xrdb', ['-query']);
|
|
307
|
+
if (result.status !== 0)
|
|
308
|
+
return [];
|
|
309
|
+
return result.stdout
|
|
310
|
+
.split('\n')
|
|
311
|
+
.map((line) => {
|
|
312
|
+
const colon = line.indexOf(':');
|
|
313
|
+
return colon === -1 ? null : [line.slice(0, colon).trim(), line.slice(colon + 1).trim()];
|
|
314
|
+
})
|
|
315
|
+
.filter((entry) => entry !== null);
|
|
316
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Which terminal emulator this process is running in.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { defaultRun } from './deps.js';
|
|
5
|
+
/** Process names of the terminals this module can read, innermost wins. */
|
|
6
|
+
const TERMINAL_PROCESSES = [
|
|
7
|
+
[/^gnome-terminal/, 'gnome-terminal'],
|
|
8
|
+
[/^ptyxis/, 'ptyxis'],
|
|
9
|
+
[/^tilix$/, 'tilix'],
|
|
10
|
+
[/^konsole$/, 'konsole'],
|
|
11
|
+
[/^xfce4-terminal$/, 'xfce4-terminal'],
|
|
12
|
+
[/^kitty$/, 'kitty'],
|
|
13
|
+
[/^alacritty$/i, 'alacritty'],
|
|
14
|
+
[/^ghostty$/i, 'ghostty'],
|
|
15
|
+
[/^wezterm-gui$/, 'wezterm'],
|
|
16
|
+
[/^foot(client)?$/, 'foot'],
|
|
17
|
+
[/^iTerm2$/, 'iterm2'],
|
|
18
|
+
[/^Terminal$/, 'apple-terminal'],
|
|
19
|
+
[/^xterm$/, 'xterm'],
|
|
20
|
+
[/^(urxvt|urxvtd|rxvt)$/, 'urxvt'],
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* The emulator a process runs under, from its own ancestry first.
|
|
24
|
+
*
|
|
25
|
+
* Environment variables leak through nested terminals — a kitty launched from
|
|
26
|
+
* GNOME Terminal still carries `GNOME_TERMINAL_SCREEN` — whereas the nearest
|
|
27
|
+
* emulator process above this one is the one actually drawing it. The
|
|
28
|
+
* environment is the fallback for the common case where there is no such
|
|
29
|
+
* ancestor: Herdr's server runs detached, so everything it starts, this
|
|
30
|
+
* connector included, has lost its terminal parent but kept its variables.
|
|
31
|
+
*/
|
|
32
|
+
export function identifyTerminal({ env = process.env, ancestry = [], } = {}) {
|
|
33
|
+
for (const name of ancestry) {
|
|
34
|
+
const base = path.basename(String(name || ''));
|
|
35
|
+
for (const [pattern, id] of TERMINAL_PROCESSES) {
|
|
36
|
+
if (pattern.test(base))
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const program = env.TERM_PROGRAM || '';
|
|
41
|
+
if (program === 'vscode')
|
|
42
|
+
return 'vscode';
|
|
43
|
+
if (program === 'iTerm.app' || env.ITERM_SESSION_ID)
|
|
44
|
+
return 'iterm2';
|
|
45
|
+
if (program === 'Apple_Terminal')
|
|
46
|
+
return 'apple-terminal';
|
|
47
|
+
if (program === 'WezTerm' || env.WEZTERM_EXECUTABLE)
|
|
48
|
+
return 'wezterm';
|
|
49
|
+
if (program === 'ghostty' || env.GHOSTTY_RESOURCES_DIR)
|
|
50
|
+
return 'ghostty';
|
|
51
|
+
if (env.KITTY_WINDOW_ID || env.TERM === 'xterm-kitty')
|
|
52
|
+
return 'kitty';
|
|
53
|
+
if (env.ALACRITTY_SOCKET || env.ALACRITTY_WINDOW_ID || env.ALACRITTY_LOG)
|
|
54
|
+
return 'alacritty';
|
|
55
|
+
if (/^foot/.test(env.TERM || ''))
|
|
56
|
+
return 'foot';
|
|
57
|
+
if (env.KONSOLE_VERSION || env.KONSOLE_DBUS_SESSION)
|
|
58
|
+
return 'konsole';
|
|
59
|
+
if (env.PTYXIS_VERSION)
|
|
60
|
+
return 'ptyxis';
|
|
61
|
+
if (env.TILIX_ID)
|
|
62
|
+
return 'tilix';
|
|
63
|
+
if (env.GNOME_TERMINAL_SCREEN || env.GNOME_TERMINAL_SERVICE)
|
|
64
|
+
return 'gnome-terminal';
|
|
65
|
+
if (/^rxvt-unicode/.test(env.TERM || ''))
|
|
66
|
+
return 'urxvt';
|
|
67
|
+
// Last: a terminal started from xterm inherits this too.
|
|
68
|
+
if (env.XTERM_VERSION)
|
|
69
|
+
return 'xterm';
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/** This process's ancestors, nearest first: `{ pid, name }`. */
|
|
73
|
+
export function processLineage({ platform = process.platform, pid = process.ppid, run = defaultRun, } = {}) {
|
|
74
|
+
const lineage = [];
|
|
75
|
+
if (platform === 'linux') {
|
|
76
|
+
let current = pid;
|
|
77
|
+
for (let depth = 0; depth < 32 && current > 1; depth += 1) {
|
|
78
|
+
let stat;
|
|
79
|
+
try {
|
|
80
|
+
stat = fs.readFileSync(`/proc/${current}/stat`, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
// `pid (comm) state ppid ...`, where comm may itself contain spaces.
|
|
86
|
+
const open = stat.indexOf('(');
|
|
87
|
+
const close = stat.lastIndexOf(')');
|
|
88
|
+
if (open === -1 || close === -1)
|
|
89
|
+
break;
|
|
90
|
+
lineage.push({ pid: current, name: stat.slice(open + 1, close) });
|
|
91
|
+
current = Number(stat.slice(close + 2).split(' ')[1]);
|
|
92
|
+
}
|
|
93
|
+
return lineage;
|
|
94
|
+
}
|
|
95
|
+
if (platform === 'darwin') {
|
|
96
|
+
const result = run('ps', ['-axo', 'pid=,ppid=,comm=']);
|
|
97
|
+
if (result.status !== 0)
|
|
98
|
+
return lineage;
|
|
99
|
+
const table = new Map();
|
|
100
|
+
for (const line of result.stdout.split('\n')) {
|
|
101
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
|
|
102
|
+
if (match)
|
|
103
|
+
table.set(Number(match[1]), { ppid: Number(match[2]), comm: match[3].trim() });
|
|
104
|
+
}
|
|
105
|
+
let current = pid;
|
|
106
|
+
for (let depth = 0; depth < 32 && table.has(current) && current > 1; depth += 1) {
|
|
107
|
+
const entry = table.get(current);
|
|
108
|
+
lineage.push({ pid: current, name: entry.comm });
|
|
109
|
+
current = entry.ppid;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return lineage;
|
|
113
|
+
}
|
|
114
|
+
/** The command line of a running process, as its argument list. */
|
|
115
|
+
export function processArguments(pid, { platform = process.platform } = {}) {
|
|
116
|
+
if (platform !== 'linux')
|
|
117
|
+
return [];
|
|
118
|
+
try {
|
|
119
|
+
return fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').filter(Boolean);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** The ancestor process that is `source`'s emulator, if any. */
|
|
126
|
+
export function terminalProcess(source, lineage) {
|
|
127
|
+
return (lineage.find((entry) => TERMINAL_PROCESSES.some(([pattern, id]) => id === source && pattern.test(path.basename(String(entry.name || ''))))) || null);
|
|
128
|
+
}
|