herdr-remote 0.2.18 → 0.2.20
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,73 @@
|
|
|
1
|
+
// What font detection reads the world through, injectable for tests, and the
|
|
2
|
+
// unit conversions every reader shares.
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
/** Terminals size fonts in points; CSS pixels are 1/96 inch. */
|
|
8
|
+
const PX_PER_PT = 96 / 72;
|
|
9
|
+
const COMMAND_TIMEOUT_MS = 3000;
|
|
10
|
+
/** Names a terminal accepts in place of a real family. */
|
|
11
|
+
export const GENERIC_FAMILIES = new Set([
|
|
12
|
+
'monospace',
|
|
13
|
+
'mono',
|
|
14
|
+
'sans',
|
|
15
|
+
'sans-serif',
|
|
16
|
+
'serif',
|
|
17
|
+
'system-ui',
|
|
18
|
+
'ui-monospace',
|
|
19
|
+
]);
|
|
20
|
+
export function defaultRun(command, args) {
|
|
21
|
+
try {
|
|
22
|
+
const result = spawnSync(command, args, { encoding: 'utf8', timeout: COMMAND_TIMEOUT_MS });
|
|
23
|
+
return { status: result.status, stdout: result.stdout || '' };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { status: null, stdout: '' };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function defaultReadFile(filePath) {
|
|
30
|
+
try {
|
|
31
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function defaultReadBuffer(filePath) {
|
|
38
|
+
try {
|
|
39
|
+
return fs.readFileSync(filePath);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function withDefaults(deps = {}) {
|
|
46
|
+
const env = deps.env || process.env;
|
|
47
|
+
return {
|
|
48
|
+
env,
|
|
49
|
+
platform: deps.platform || process.platform,
|
|
50
|
+
home: deps.home || env.HOME || os.homedir(),
|
|
51
|
+
run: deps.run || defaultRun,
|
|
52
|
+
readFile: deps.readFile || defaultReadFile,
|
|
53
|
+
readBuffer: deps.readBuffer || defaultReadBuffer,
|
|
54
|
+
terminalArgs: deps.terminalArgs || [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function configHome(deps) {
|
|
58
|
+
return deps.env.XDG_CONFIG_HOME || path.join(deps.home, '.config');
|
|
59
|
+
}
|
|
60
|
+
export function dataHome(deps) {
|
|
61
|
+
return deps.env.XDG_DATA_HOME || path.join(deps.home, '.local', 'share');
|
|
62
|
+
}
|
|
63
|
+
export function expandHome(filePath, deps) {
|
|
64
|
+
return filePath.startsWith('~/') ? path.join(deps.home, filePath.slice(2)) : filePath;
|
|
65
|
+
}
|
|
66
|
+
export function pointsToPx(points) {
|
|
67
|
+
const value = Number(points);
|
|
68
|
+
return Number.isFinite(value) && value > 0 ? Math.round(value * PX_PER_PT * 10) / 10 : undefined;
|
|
69
|
+
}
|
|
70
|
+
export function positiveNumber(value) {
|
|
71
|
+
const number = Number(value);
|
|
72
|
+
return Number.isFinite(number) && number > 0 ? number : undefined;
|
|
73
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// The files behind a font family: found with fontconfig (or, on a stock
|
|
2
|
+
// macOS, by reading the fonts' own name tables) and described by hash.
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { MAX_TERMINAL_FONT_BYTES, TERMINAL_FONT_STYLES, } from 'herdr-remote-relay/protocol';
|
|
7
|
+
import { GENERIC_FAMILIES, withDefaults } from './deps.js';
|
|
8
|
+
const FACE_PATTERNS = {
|
|
9
|
+
regular: ':weight=regular:slant=roman',
|
|
10
|
+
bold: ':weight=bold:slant=roman',
|
|
11
|
+
italic: ':weight=regular:slant=italic',
|
|
12
|
+
boldItalic: ':weight=bold:slant=italic',
|
|
13
|
+
};
|
|
14
|
+
function escapeFontconfig(value) {
|
|
15
|
+
return value.replace(/([\\\-:,])/g, '\\$1');
|
|
16
|
+
}
|
|
17
|
+
/** `00 01 00 00` / `true` are TrueType, `OTTO` is CFF OpenType. */
|
|
18
|
+
function sfntFormat(header) {
|
|
19
|
+
if (!header || header.length < 4)
|
|
20
|
+
return null;
|
|
21
|
+
const tag = header.subarray(0, 4).toString('latin1');
|
|
22
|
+
if (tag === '\x00\x01\x00\x00' || tag === 'true')
|
|
23
|
+
return 'truetype';
|
|
24
|
+
if (tag === 'OTTO')
|
|
25
|
+
return 'opentype';
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function readHeader(filePath, length) {
|
|
29
|
+
let fd;
|
|
30
|
+
try {
|
|
31
|
+
fd = fs.openSync(filePath, 'r');
|
|
32
|
+
const buffer = Buffer.alloc(length);
|
|
33
|
+
const read = fs.readSync(fd, buffer, 0, length, 0);
|
|
34
|
+
return buffer.subarray(0, read);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
if (fd !== undefined)
|
|
41
|
+
try {
|
|
42
|
+
fs.closeSync(fd);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Closing a read-only descriptor cannot lose data.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The family, subfamily and PostScript names in a TTF/OTF `name` table. Used
|
|
51
|
+
* where fontconfig is absent — a stock macOS — to find a family's files.
|
|
52
|
+
*/
|
|
53
|
+
function readSfntNames(filePath) {
|
|
54
|
+
let fd;
|
|
55
|
+
try {
|
|
56
|
+
fd = fs.openSync(filePath, 'r');
|
|
57
|
+
const head = Buffer.alloc(12);
|
|
58
|
+
fs.readSync(fd, head, 0, 12, 0);
|
|
59
|
+
if (!sfntFormat(head))
|
|
60
|
+
return null;
|
|
61
|
+
const tables = head.readUInt16BE(4);
|
|
62
|
+
const directory = Buffer.alloc(tables * 16);
|
|
63
|
+
fs.readSync(fd, directory, 0, directory.length, 12);
|
|
64
|
+
let offset = -1;
|
|
65
|
+
let length = 0;
|
|
66
|
+
for (let i = 0; i < tables; i += 1) {
|
|
67
|
+
if (directory.toString('latin1', i * 16, i * 16 + 4) === 'name') {
|
|
68
|
+
offset = directory.readUInt32BE(i * 16 + 8);
|
|
69
|
+
length = directory.readUInt32BE(i * 16 + 12);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (offset < 0 || length > 1024 * 1024)
|
|
73
|
+
return null;
|
|
74
|
+
const table = Buffer.alloc(length);
|
|
75
|
+
fs.readSync(fd, table, 0, length, offset);
|
|
76
|
+
const count = table.readUInt16BE(2);
|
|
77
|
+
const strings = table.readUInt16BE(4);
|
|
78
|
+
const names = {};
|
|
79
|
+
for (let i = 0; i < count; i += 1) {
|
|
80
|
+
const record = 6 + i * 12;
|
|
81
|
+
const platformId = table.readUInt16BE(record);
|
|
82
|
+
const languageId = table.readUInt16BE(record + 4);
|
|
83
|
+
const nameId = table.readUInt16BE(record + 6);
|
|
84
|
+
const size = table.readUInt16BE(record + 8);
|
|
85
|
+
const start = strings + table.readUInt16BE(record + 10);
|
|
86
|
+
if (![1, 2, 6, 16, 17].includes(nameId) || start + size > table.length)
|
|
87
|
+
continue;
|
|
88
|
+
let text;
|
|
89
|
+
if ((platformId === 3 || platformId === 0) && size % 2 === 0) {
|
|
90
|
+
if (platformId === 3 && languageId !== 0x409 && names[nameId])
|
|
91
|
+
continue;
|
|
92
|
+
const utf16 = Buffer.from(table.subarray(start, start + size));
|
|
93
|
+
text = utf16.swap16().toString('utf16le');
|
|
94
|
+
}
|
|
95
|
+
else if (platformId === 1 && !names[nameId]) {
|
|
96
|
+
text = table.toString('latin1', start, start + size);
|
|
97
|
+
}
|
|
98
|
+
if (text)
|
|
99
|
+
names[nameId] = text;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
family: names[16] || names[1] || null,
|
|
103
|
+
subfamily: names[17] || names[2] || '',
|
|
104
|
+
postscript: names[6] || null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
if (fd !== undefined)
|
|
112
|
+
try {
|
|
113
|
+
fs.closeSync(fd);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Closing a read-only descriptor cannot lose data.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function macFontFiles(deps) {
|
|
121
|
+
const roots = [
|
|
122
|
+
path.join(deps.home, 'Library', 'Fonts'),
|
|
123
|
+
'/Library/Fonts',
|
|
124
|
+
'/System/Library/Fonts',
|
|
125
|
+
];
|
|
126
|
+
const files = [];
|
|
127
|
+
const walk = (directory, depth) => {
|
|
128
|
+
let entries;
|
|
129
|
+
try {
|
|
130
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
const full = path.join(directory, entry.name);
|
|
137
|
+
if (entry.isDirectory() && depth < 2)
|
|
138
|
+
walk(full, depth + 1);
|
|
139
|
+
else if (/\.(ttf|otf)$/i.test(entry.name))
|
|
140
|
+
files.push(full);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
for (const root of roots)
|
|
144
|
+
walk(root, 0);
|
|
145
|
+
return files;
|
|
146
|
+
}
|
|
147
|
+
/** Apple's own terminal faces, which live outside the font folders. */
|
|
148
|
+
const APPLE_POSTSCRIPT_FAMILIES = [
|
|
149
|
+
[/^\.?SF(NS)?Mono/, 'SF Mono'],
|
|
150
|
+
[/^Menlo-/, 'Menlo'],
|
|
151
|
+
[/^Monaco$/, 'Monaco'],
|
|
152
|
+
[/^Courier(New)?/, 'Courier New'],
|
|
153
|
+
[/^AndaleMono$/, 'Andale Mono'],
|
|
154
|
+
];
|
|
155
|
+
export function familyForPostScriptName(postscript, deps) {
|
|
156
|
+
const apple = APPLE_POSTSCRIPT_FAMILIES.find(([pattern]) => pattern.test(postscript));
|
|
157
|
+
if (apple && deps.platform === 'darwin')
|
|
158
|
+
return apple[1];
|
|
159
|
+
const listed = deps.run('fc-list', [`:postscriptname=${escapeFontconfig(postscript)}`, 'family']);
|
|
160
|
+
if (listed.status === 0 && listed.stdout.trim())
|
|
161
|
+
return listed.stdout.trim().split('\n')[0].split(',')[0].trim();
|
|
162
|
+
if (deps.platform !== 'darwin')
|
|
163
|
+
return null;
|
|
164
|
+
for (const file of macFontFiles(deps)) {
|
|
165
|
+
const names = readSfntNames(file);
|
|
166
|
+
if (names?.postscript === postscript)
|
|
167
|
+
return names.family;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
function styleOfSubfamily(subfamily) {
|
|
172
|
+
const text = String(subfamily || '').toLowerCase();
|
|
173
|
+
const bold = /\bbold\b/.test(text) && !/(semi|demi|extra|ultra)\s*bold/.test(text);
|
|
174
|
+
const italic = /italic|oblique/.test(text);
|
|
175
|
+
if (bold && italic)
|
|
176
|
+
return 'boldItalic';
|
|
177
|
+
if (bold)
|
|
178
|
+
return 'bold';
|
|
179
|
+
if (italic)
|
|
180
|
+
return 'italic';
|
|
181
|
+
return /^(regular|normal|book|roman)$/.test(text.trim()) ? 'regular' : null;
|
|
182
|
+
}
|
|
183
|
+
/** Candidate files per style, from fontconfig or, failing that, a scan. */
|
|
184
|
+
function locateFaceFiles(family, deps) {
|
|
185
|
+
const located = {};
|
|
186
|
+
let fontconfig = false;
|
|
187
|
+
for (const style of TERMINAL_FONT_STYLES) {
|
|
188
|
+
const result = deps.run('fc-match', [
|
|
189
|
+
'-f',
|
|
190
|
+
'%{file}\n%{family}\n%{index}\n',
|
|
191
|
+
`${escapeFontconfig(family)}${FACE_PATTERNS[style]}`,
|
|
192
|
+
]);
|
|
193
|
+
if (result.status !== 0)
|
|
194
|
+
continue;
|
|
195
|
+
fontconfig = true;
|
|
196
|
+
const [file, families = '', index = '0'] = result.stdout.split('\n');
|
|
197
|
+
// fontconfig always answers; an answer in another family means the
|
|
198
|
+
// requested one is not installed. A collection member is not a file a
|
|
199
|
+
// browser can load on its own.
|
|
200
|
+
const names = families.split(',').map((name) => name.trim().toLowerCase());
|
|
201
|
+
if (!file || !names.includes(family.toLowerCase()) || Number(index) !== 0)
|
|
202
|
+
continue;
|
|
203
|
+
located[style] = file;
|
|
204
|
+
}
|
|
205
|
+
if (fontconfig || deps.platform !== 'darwin')
|
|
206
|
+
return located;
|
|
207
|
+
for (const file of macFontFiles(deps)) {
|
|
208
|
+
const names = readSfntNames(file);
|
|
209
|
+
if (!names || names.family?.toLowerCase() !== family.toLowerCase())
|
|
210
|
+
continue;
|
|
211
|
+
const style = styleOfSubfamily(names.subfamily);
|
|
212
|
+
if (style && !located[style])
|
|
213
|
+
located[style] = file;
|
|
214
|
+
}
|
|
215
|
+
return located;
|
|
216
|
+
}
|
|
217
|
+
function hashFile(filePath) {
|
|
218
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The files behind a family, ready to be offered to a browser.
|
|
222
|
+
*
|
|
223
|
+
* A style fontconfig can only satisfy with the regular file is left out: the
|
|
224
|
+
* browser synthesises bold and italic from the regular face just as well, and
|
|
225
|
+
* sending the same file twice would only cost the phone the bytes.
|
|
226
|
+
*/
|
|
227
|
+
export function resolveFontFaces(family, deps = {}) {
|
|
228
|
+
const resolved = withDefaults(deps);
|
|
229
|
+
if (!family || GENERIC_FAMILIES.has(family.toLowerCase()))
|
|
230
|
+
return [];
|
|
231
|
+
const located = locateFaceFiles(family, resolved);
|
|
232
|
+
const faces = [];
|
|
233
|
+
const usedFiles = new Set();
|
|
234
|
+
for (const style of TERMINAL_FONT_STYLES) {
|
|
235
|
+
const file = located[style];
|
|
236
|
+
if (!file || usedFiles.has(file))
|
|
237
|
+
continue;
|
|
238
|
+
// Apple's system fonts (SF Mono, Menlo…) are licensed for Apple devices
|
|
239
|
+
// only; offering them to any browser would redistribute them. An Apple
|
|
240
|
+
// device has them already, and anything else falls back by name.
|
|
241
|
+
if (/^\/System\//.test(file) || /\.app\/Contents\//.test(file))
|
|
242
|
+
continue;
|
|
243
|
+
if (style !== 'regular' && !faces.length)
|
|
244
|
+
continue;
|
|
245
|
+
let stat;
|
|
246
|
+
try {
|
|
247
|
+
stat = fs.statSync(file);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_TERMINAL_FONT_BYTES)
|
|
253
|
+
continue;
|
|
254
|
+
const format = sfntFormat(readHeader(file, 4));
|
|
255
|
+
if (!format)
|
|
256
|
+
continue;
|
|
257
|
+
usedFiles.add(file);
|
|
258
|
+
faces.push({
|
|
259
|
+
style,
|
|
260
|
+
format,
|
|
261
|
+
bytes: stat.size,
|
|
262
|
+
sha256: hashFile(file),
|
|
263
|
+
path: file,
|
|
264
|
+
mtimeMs: stat.mtimeMs,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return faces;
|
|
268
|
+
}
|
|
269
|
+
/** A file a subset can be cut from; far larger than a face sent whole. */
|
|
270
|
+
const MAX_SUBSET_SOURCE_BYTES = 64 * 1024 * 1024;
|
|
271
|
+
/** Where fontconfig finds `pattern`: the file, the face's names, its index. */
|
|
272
|
+
function locateFont(pattern, deps) {
|
|
273
|
+
const result = deps.run('fc-match', ['-f', '%{file}\n%{family}\n%{index}\n', pattern]);
|
|
274
|
+
if (result.status !== 0)
|
|
275
|
+
return null;
|
|
276
|
+
const [file, families = '', index = '0'] = result.stdout.split('\n');
|
|
277
|
+
if (!file)
|
|
278
|
+
return null;
|
|
279
|
+
return {
|
|
280
|
+
file,
|
|
281
|
+
families: families
|
|
282
|
+
.split(',')
|
|
283
|
+
.map((name) => name.trim())
|
|
284
|
+
.filter(Boolean),
|
|
285
|
+
index: Number(index) || 0,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Fonts the browser receives a few characters at a time instead of whole:
|
|
290
|
+
*
|
|
291
|
+
* - `cjk`: the face fontconfig falls back to for Chinese under this family —
|
|
292
|
+
* what the workstation's terminal actually draws Hanzi with. 16–20 MB, and
|
|
293
|
+
* often inside a collection (.ttc) a browser cannot load anyway.
|
|
294
|
+
* - `all`: the family itself, when it is too large or in a collection to be
|
|
295
|
+
* sent as a whole file (a CJK programming font like Sarasa Mono).
|
|
296
|
+
*
|
|
297
|
+
* Only regular weights: the browser emboldens them for bold text, which is
|
|
298
|
+
* how a missing bold is drawn everywhere else too.
|
|
299
|
+
*/
|
|
300
|
+
export function resolveSubsetSources(family, faces, deps = {}) {
|
|
301
|
+
const resolved = withDefaults(deps);
|
|
302
|
+
if (!family || GENERIC_FAMILIES.has(family.toLowerCase()))
|
|
303
|
+
return [];
|
|
304
|
+
const wanted = family.toLowerCase();
|
|
305
|
+
const sources = [];
|
|
306
|
+
const add = (located, scope, name) => {
|
|
307
|
+
if (!located || /^\/System\//.test(located.file) || /\.app\/Contents\//.test(located.file))
|
|
308
|
+
return;
|
|
309
|
+
let stat;
|
|
310
|
+
try {
|
|
311
|
+
stat = fs.statSync(located.file);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!stat.isFile() || stat.size > MAX_SUBSET_SOURCE_BYTES)
|
|
317
|
+
return;
|
|
318
|
+
const header = readHeader(located.file, 4);
|
|
319
|
+
if (!sfntFormat(header) && header?.toString('latin1') !== 'ttcf')
|
|
320
|
+
return;
|
|
321
|
+
// Identifies the face without reading 20 MB: a browser caches by it.
|
|
322
|
+
const sha256 = crypto
|
|
323
|
+
.createHash('sha256')
|
|
324
|
+
.update(`${located.file}\0${located.index}\0${stat.size}\0${stat.mtimeMs}`)
|
|
325
|
+
.digest('hex');
|
|
326
|
+
sources.push({
|
|
327
|
+
family: name,
|
|
328
|
+
style: 'regular',
|
|
329
|
+
scope,
|
|
330
|
+
sha256,
|
|
331
|
+
path: located.file,
|
|
332
|
+
index: located.index,
|
|
333
|
+
bytes: stat.size,
|
|
334
|
+
mtimeMs: stat.mtimeMs,
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
const self = locateFont(`${escapeFontconfig(family)}${FACE_PATTERNS.regular}`, resolved);
|
|
338
|
+
const selfInstalled = self?.families.some((name) => name.toLowerCase() === wanted);
|
|
339
|
+
if (selfInstalled && !faces.some((face) => face.style === 'regular'))
|
|
340
|
+
add(self, 'all', family);
|
|
341
|
+
const cjk = locateFont(`${escapeFontconfig(family)}:charset=4e00${FACE_PATTERNS.regular}`, resolved);
|
|
342
|
+
const coveredBySelf = cjk?.families.some((name) => name.toLowerCase() === wanted);
|
|
343
|
+
if (cjk && !coveredBySelf && cjk.families[0])
|
|
344
|
+
add(cjk, 'cjk', cjk.families[0]);
|
|
345
|
+
return sources;
|
|
346
|
+
}
|
|
347
|
+
/** A generic family is replaced by the real face fontconfig picks for it. */
|
|
348
|
+
export function concreteFamily(family, deps) {
|
|
349
|
+
if (!GENERIC_FAMILIES.has(String(family).toLowerCase()))
|
|
350
|
+
return family;
|
|
351
|
+
const result = deps.run('fc-match', ['-f', '%{family[0]}', family]);
|
|
352
|
+
const matched = result.status === 0 ? result.stdout.trim() : '';
|
|
353
|
+
return matched || null;
|
|
354
|
+
}
|