herdr-remote 0.2.17 → 0.2.18

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/tui.mjs CHANGED
@@ -1827,6 +1827,1161 @@ var require_terminal_palette = __commonJS({
1827
1827
  }
1828
1828
  });
1829
1829
 
1830
+ // src/binary-plist.js
1831
+ var require_binary_plist = __commonJS({
1832
+ "src/binary-plist.js"(exports, module) {
1833
+ "use strict";
1834
+ var Uid = class {
1835
+ constructor(value) {
1836
+ this.value = value;
1837
+ }
1838
+ };
1839
+ function readSizedInt(buffer, offset, size) {
1840
+ let value = 0;
1841
+ for (let i = 0; i < size; i += 1) value = value * 256 + buffer[offset + i];
1842
+ return value;
1843
+ }
1844
+ function parseBinaryPlist(buffer) {
1845
+ if (!Buffer.isBuffer(buffer) || buffer.length < 40 || buffer.toString("latin1", 0, 8) !== "bplist00") {
1846
+ throw new Error("not a binary plist");
1847
+ }
1848
+ const trailer = buffer.length - 32;
1849
+ const offsetSize = buffer[trailer + 6];
1850
+ const refSize = buffer[trailer + 7];
1851
+ const count = readSizedInt(buffer, trailer + 8, 8);
1852
+ const top = readSizedInt(buffer, trailer + 16, 8);
1853
+ const tableOffset = readSizedInt(buffer, trailer + 24, 8);
1854
+ if (!offsetSize || !refSize || count > 1e6 || tableOffset + count * offsetSize > trailer) {
1855
+ throw new Error("corrupt binary plist trailer");
1856
+ }
1857
+ const offsets = [];
1858
+ for (let i = 0; i < count; i += 1) offsets.push(readSizedInt(buffer, tableOffset + i * offsetSize, offsetSize));
1859
+ const parsing = /* @__PURE__ */ new Set();
1860
+ const parse = (index) => {
1861
+ if (index >= count || parsing.has(index)) throw new Error("corrupt binary plist reference");
1862
+ parsing.add(index);
1863
+ try {
1864
+ return parseObject(offsets[index]);
1865
+ } finally {
1866
+ parsing.delete(index);
1867
+ }
1868
+ };
1869
+ const lengthAt = (offset) => {
1870
+ const low = buffer[offset] & 15;
1871
+ if (low !== 15) return { length: low, start: offset + 1 };
1872
+ const marker = buffer[offset + 1];
1873
+ if ((marker & 240) !== 16) throw new Error("corrupt binary plist length");
1874
+ const size = 1 << (marker & 15);
1875
+ return { length: readSizedInt(buffer, offset + 2, size), start: offset + 2 + size };
1876
+ };
1877
+ const parseObject = (offset) => {
1878
+ const marker = buffer[offset];
1879
+ const type = marker >> 4;
1880
+ const low = marker & 15;
1881
+ switch (type) {
1882
+ case 0:
1883
+ if (marker === 8) return false;
1884
+ if (marker === 9) return true;
1885
+ return null;
1886
+ case 1: {
1887
+ const size = 1 << low;
1888
+ if (size === 8) return Number(buffer.readBigInt64BE(offset + 1));
1889
+ return readSizedInt(buffer, offset + 1, size);
1890
+ }
1891
+ case 2:
1892
+ return low === 2 ? buffer.readFloatBE(offset + 1) : buffer.readDoubleBE(offset + 1);
1893
+ case 3:
1894
+ return new Date(Date.UTC(2001, 0, 1) + buffer.readDoubleBE(offset + 1) * 1e3);
1895
+ case 4: {
1896
+ const { length, start } = lengthAt(offset);
1897
+ return buffer.subarray(start, start + length);
1898
+ }
1899
+ case 5: {
1900
+ const { length, start } = lengthAt(offset);
1901
+ return buffer.toString("latin1", start, start + length);
1902
+ }
1903
+ case 6: {
1904
+ const { length, start } = lengthAt(offset);
1905
+ return Buffer.from(buffer.subarray(start, start + length * 2)).swap16().toString("utf16le");
1906
+ }
1907
+ case 8:
1908
+ return new Uid(readSizedInt(buffer, offset + 1, low + 1));
1909
+ case 10:
1910
+ case 12: {
1911
+ const { length, start } = lengthAt(offset);
1912
+ const items = [];
1913
+ for (let i = 0; i < length; i += 1) items.push(parse(readSizedInt(buffer, start + i * refSize, refSize)));
1914
+ return items;
1915
+ }
1916
+ case 13: {
1917
+ const { length, start } = lengthAt(offset);
1918
+ const result = {};
1919
+ for (let i = 0; i < length; i += 1) {
1920
+ const key = parse(readSizedInt(buffer, start + i * refSize, refSize));
1921
+ result[String(key)] = parse(readSizedInt(buffer, start + (length + i) * refSize, refSize));
1922
+ }
1923
+ return result;
1924
+ }
1925
+ default:
1926
+ throw new Error(`unsupported binary plist object 0x${marker.toString(16)}`);
1927
+ }
1928
+ };
1929
+ return parse(top);
1930
+ }
1931
+ function unarchiveKeyed(archive, depth = 6) {
1932
+ const objects = archive?.$objects;
1933
+ const rootRef = archive?.$top?.root;
1934
+ if (!Array.isArray(objects) || !(rootRef instanceof Uid)) throw new Error("not a keyed archive");
1935
+ const resolve = (value, level2) => {
1936
+ if (value instanceof Uid) return level2 > depth ? null : resolve(objects[value.value], level2 + 1);
1937
+ if (Array.isArray(value)) return value.map((item) => resolve(item, level2));
1938
+ if (value && typeof value === "object" && !Buffer.isBuffer(value) && !(value instanceof Date)) {
1939
+ const result = {};
1940
+ for (const [key, item] of Object.entries(value)) result[key] = resolve(item, level2);
1941
+ return result;
1942
+ }
1943
+ return value === "$null" ? null : value;
1944
+ };
1945
+ return resolve(rootRef, 0);
1946
+ }
1947
+ module.exports = { Uid, parseBinaryPlist, unarchiveKeyed };
1948
+ }
1949
+ });
1950
+
1951
+ // src/terminal-font.js
1952
+ var require_terminal_font = __commonJS({
1953
+ "src/terminal-font.js"(exports, module) {
1954
+ "use strict";
1955
+ var crypto = __require("node:crypto");
1956
+ var fs = __require("node:fs");
1957
+ var os = __require("node:os");
1958
+ var path = __require("node:path");
1959
+ var { spawnSync } = __require("node:child_process");
1960
+ var {
1961
+ MAX_TERMINAL_FONT_BYTES,
1962
+ TERMINAL_FONT_CHUNK_BYTES,
1963
+ TERMINAL_FONT_STYLES,
1964
+ sanitizeTerminalFont
1965
+ } = __require("herdr-remote-relay/protocol");
1966
+ var { runtimeStatePath, stateDir: stateDir2 } = require_config();
1967
+ var { ensureDir, readJson, writeJsonAtomic } = require_state();
1968
+ var { parseBinaryPlist, unarchiveKeyed } = require_binary_plist();
1969
+ var PX_PER_PT = 96 / 72;
1970
+ var COMMAND_TIMEOUT_MS = 3e3;
1971
+ var GENERIC_FAMILIES = /* @__PURE__ */ new Set(["monospace", "mono", "sans", "sans-serif", "serif", "system-ui", "ui-monospace"]);
1972
+ function defaultRun(command, args) {
1973
+ try {
1974
+ const result = spawnSync(command, args, { encoding: "utf8", timeout: COMMAND_TIMEOUT_MS });
1975
+ return { status: result.status, stdout: result.stdout || "" };
1976
+ } catch {
1977
+ return { status: null, stdout: "" };
1978
+ }
1979
+ }
1980
+ function defaultReadFile(filePath) {
1981
+ try {
1982
+ return fs.readFileSync(filePath, "utf8");
1983
+ } catch {
1984
+ return null;
1985
+ }
1986
+ }
1987
+ function defaultReadBuffer(filePath) {
1988
+ try {
1989
+ return fs.readFileSync(filePath);
1990
+ } catch {
1991
+ return null;
1992
+ }
1993
+ }
1994
+ function withDefaults(deps = {}) {
1995
+ const env = deps.env || process.env;
1996
+ return {
1997
+ env,
1998
+ platform: deps.platform || process.platform,
1999
+ home: deps.home || env.HOME || os.homedir(),
2000
+ run: deps.run || defaultRun,
2001
+ readFile: deps.readFile || defaultReadFile,
2002
+ readBuffer: deps.readBuffer || defaultReadBuffer,
2003
+ terminalArgs: deps.terminalArgs || []
2004
+ };
2005
+ }
2006
+ function configHome(deps) {
2007
+ return deps.env.XDG_CONFIG_HOME || path.join(deps.home, ".config");
2008
+ }
2009
+ function dataHome(deps) {
2010
+ return deps.env.XDG_DATA_HOME || path.join(deps.home, ".local", "share");
2011
+ }
2012
+ function expandHome(filePath, deps) {
2013
+ return filePath.startsWith("~/") ? path.join(deps.home, filePath.slice(2)) : filePath;
2014
+ }
2015
+ function pointsToPx(points) {
2016
+ const value = Number(points);
2017
+ return Number.isFinite(value) && value > 0 ? Math.round(value * PX_PER_PT * 10) / 10 : void 0;
2018
+ }
2019
+ function positiveNumber(value) {
2020
+ const number = Number(value);
2021
+ return Number.isFinite(number) && number > 0 ? number : void 0;
2022
+ }
2023
+ function parseIni(text) {
2024
+ const sections = { "": {} };
2025
+ let current = sections[""];
2026
+ for (const rawLine of String(text || "").split(/\r?\n/)) {
2027
+ const line = rawLine.trim();
2028
+ if (!line || line.startsWith("#") || line.startsWith(";")) continue;
2029
+ const header = /^\[(.+)\]$/.exec(line);
2030
+ if (header) {
2031
+ current = sections[header[1].trim()] ||= {};
2032
+ continue;
2033
+ }
2034
+ const equals = line.indexOf("=");
2035
+ if (equals === -1) continue;
2036
+ current[line.slice(0, equals).trim()] = line.slice(equals + 1).trim();
2037
+ }
2038
+ return sections;
2039
+ }
2040
+ function parseGVariantString(output) {
2041
+ const text = String(output || "").trim();
2042
+ const quoted = /^'((?:[^'\\]|\\.)*)'$/.exec(text) || /^"((?:[^"\\]|\\.)*)"$/.exec(text);
2043
+ if (!quoted) return null;
2044
+ return quoted[1].replace(/\\(.)/g, "$1");
2045
+ }
2046
+ function gsettingsGet(deps, schema, key) {
2047
+ const result = deps.run("gsettings", ["get", schema, key]);
2048
+ if (result.status !== 0) return null;
2049
+ return result.stdout.trim();
2050
+ }
2051
+ var PANGO_STYLE_WORDS = /* @__PURE__ */ new Set([
2052
+ "normal",
2053
+ "roman",
2054
+ "oblique",
2055
+ "italic",
2056
+ "small-caps",
2057
+ "all-small-caps",
2058
+ "petite-caps",
2059
+ "all-petite-caps",
2060
+ "unicase",
2061
+ "title-caps",
2062
+ "ultra-condensed",
2063
+ "extra-condensed",
2064
+ "condensed",
2065
+ "semi-condensed",
2066
+ "semi-expanded",
2067
+ "expanded",
2068
+ "extra-expanded",
2069
+ "ultra-expanded",
2070
+ "thin",
2071
+ "ultra-light",
2072
+ "extra-light",
2073
+ "light",
2074
+ "semi-light",
2075
+ "demi-light",
2076
+ "book",
2077
+ "regular",
2078
+ "medium",
2079
+ "semi-bold",
2080
+ "demi-bold",
2081
+ "bold",
2082
+ "ultra-bold",
2083
+ "extra-bold",
2084
+ "heavy",
2085
+ "black",
2086
+ "ultra-black",
2087
+ "extra-black",
2088
+ "ultra-heavy",
2089
+ "extra-heavy",
2090
+ "not-rotated",
2091
+ "south",
2092
+ "upside-down",
2093
+ "north",
2094
+ "rotated-left",
2095
+ "east",
2096
+ "rotated-right",
2097
+ "west"
2098
+ ]);
2099
+ function parsePangoFontDescription(description) {
2100
+ if (typeof description !== "string") return null;
2101
+ const words = description.trim().split(/\s+/).filter(Boolean);
2102
+ while (words.length && words[words.length - 1].startsWith("@")) words.pop();
2103
+ let sizePx;
2104
+ const size = /^(\d+(?:\.\d+)?)(px)?$/i.exec(words[words.length - 1] || "");
2105
+ if (size && words.length > 1) {
2106
+ words.pop();
2107
+ sizePx = size[2] ? positiveNumber(size[1]) : pointsToPx(size[1]);
2108
+ }
2109
+ while (words.length > 1) {
2110
+ const last = words[words.length - 1].toLowerCase();
2111
+ if (!PANGO_STYLE_WORDS.has(last) && !/^weight=\d+$/.test(last)) break;
2112
+ words.pop();
2113
+ }
2114
+ const family = words.join(" ").split(",")[0].trim();
2115
+ return family ? { family, sizePx } : null;
2116
+ }
2117
+ function parseQtFontString(value) {
2118
+ if (typeof value !== "string") return null;
2119
+ const parts = value.split(",");
2120
+ const family = (parts[0] || "").trim();
2121
+ if (!family) return null;
2122
+ const points = Number(parts[1]);
2123
+ const pixels = Number(parts[2]);
2124
+ const sizePx = points > 0 ? pointsToPx(points) : pixels > 0 ? pixels : void 0;
2125
+ return { family, sizePx };
2126
+ }
2127
+ function parseFontconfigPattern(value) {
2128
+ if (typeof value !== "string") return null;
2129
+ const first = value.split(/(?<!\\),/)[0];
2130
+ const [rawFamily, ...properties] = first.split(/(?<!\\):/);
2131
+ const family = rawFamily.replace(/\\(.)/g, "$1").trim();
2132
+ if (!family) return null;
2133
+ let sizePx;
2134
+ for (const property of properties) {
2135
+ const [key, raw] = property.split("=");
2136
+ if (key === "size") sizePx = pointsToPx(raw);
2137
+ else if (key === "pixelsize") sizePx = positiveNumber(raw);
2138
+ }
2139
+ return { family, sizePx };
2140
+ }
2141
+ function firstCssFamily(value) {
2142
+ if (typeof value !== "string") return null;
2143
+ const first = value.split(",")[0].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
2144
+ return first || null;
2145
+ }
2146
+ function parseJsonc(text) {
2147
+ if (typeof text !== "string") return null;
2148
+ let out = "";
2149
+ let inString = false;
2150
+ for (let i = 0; i < text.length; i += 1) {
2151
+ const char = text[i];
2152
+ if (inString) {
2153
+ out += char;
2154
+ if (char === "\\") {
2155
+ out += text[i + 1] || "";
2156
+ i += 1;
2157
+ } else if (char === '"') inString = false;
2158
+ continue;
2159
+ }
2160
+ if (char === '"') {
2161
+ inString = true;
2162
+ out += char;
2163
+ continue;
2164
+ }
2165
+ if (char === "/" && text[i + 1] === "/") {
2166
+ while (i < text.length && text[i] !== "\n") i += 1;
2167
+ out += "\n";
2168
+ continue;
2169
+ }
2170
+ if (char === "/" && text[i + 1] === "*") {
2171
+ i += 2;
2172
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i += 1;
2173
+ i += 1;
2174
+ continue;
2175
+ }
2176
+ out += char;
2177
+ }
2178
+ try {
2179
+ return JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
2180
+ } catch {
2181
+ return null;
2182
+ }
2183
+ }
2184
+ function parseTomlSubset(text) {
2185
+ const values = {};
2186
+ const lines = String(text || "").split(/\r?\n/);
2187
+ let table = "";
2188
+ const unquote = (raw) => {
2189
+ const value = raw.trim();
2190
+ const basic = /^"((?:[^"\\]|\\.)*)"/.exec(value);
2191
+ if (basic) return basic[1].replace(/\\(.)/g, "$1");
2192
+ const literal = /^'([^']*)'/.exec(value);
2193
+ if (literal) return literal[1];
2194
+ const number = Number(value.replace(/#.*$/, "").trim());
2195
+ return Number.isFinite(number) ? number : void 0;
2196
+ };
2197
+ const keyPath = (raw) => raw.split(".").map((part) => part.trim().replace(/^(['"])(.*)\1$/, "$2"));
2198
+ const assign = (prefix, rawKey, rawValue) => {
2199
+ const key = [...prefix ? [prefix] : [], ...keyPath(rawKey)].join(".");
2200
+ const value = rawValue.trim();
2201
+ if (value.startsWith("{")) {
2202
+ const body = value.slice(1, value.lastIndexOf("}"));
2203
+ for (const pair2 of body.split(/,(?=(?:[^"']|"[^"]*"|'[^']*')*$)/)) {
2204
+ const equals = pair2.indexOf("=");
2205
+ if (equals !== -1) assign(key, pair2.slice(0, equals), pair2.slice(equals + 1));
2206
+ }
2207
+ return;
2208
+ }
2209
+ if (value.startsWith("[")) {
2210
+ values[key] = [...value.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2]);
2211
+ return;
2212
+ }
2213
+ const parsed = unquote(value);
2214
+ if (parsed !== void 0) values[key] = parsed;
2215
+ };
2216
+ for (let index = 0; index < lines.length; index += 1) {
2217
+ const line = lines[index].trim();
2218
+ if (!line || line.startsWith("#")) continue;
2219
+ const header = /^\[([^[\]]+)\]/.exec(line);
2220
+ if (header) {
2221
+ table = keyPath(header[1]).join(".");
2222
+ continue;
2223
+ }
2224
+ if (line.startsWith("[[")) {
2225
+ table = "\0";
2226
+ continue;
2227
+ }
2228
+ if (table === "\0") continue;
2229
+ const equals = line.indexOf("=");
2230
+ if (equals === -1) continue;
2231
+ let rawValue = line.slice(equals + 1);
2232
+ if (rawValue.trim().startsWith("[") && !rawValue.includes("]")) {
2233
+ while (index + 1 < lines.length && !rawValue.includes("]")) {
2234
+ index += 1;
2235
+ rawValue += ` ${lines[index].trim()}`;
2236
+ }
2237
+ }
2238
+ assign(table, line.slice(0, equals), rawValue);
2239
+ }
2240
+ return values;
2241
+ }
2242
+ var TERMINAL_PROCESSES = [
2243
+ [/^gnome-terminal/, "gnome-terminal"],
2244
+ [/^ptyxis/, "ptyxis"],
2245
+ [/^tilix$/, "tilix"],
2246
+ [/^konsole$/, "konsole"],
2247
+ [/^xfce4-terminal$/, "xfce4-terminal"],
2248
+ [/^kitty$/, "kitty"],
2249
+ [/^alacritty$/i, "alacritty"],
2250
+ [/^ghostty$/i, "ghostty"],
2251
+ [/^wezterm-gui$/, "wezterm"],
2252
+ [/^foot(client)?$/, "foot"],
2253
+ [/^iTerm2$/, "iterm2"],
2254
+ [/^Terminal$/, "apple-terminal"],
2255
+ [/^xterm$/, "xterm"],
2256
+ [/^(urxvt|urxvtd|rxvt)$/, "urxvt"]
2257
+ ];
2258
+ function identifyTerminal({ env = process.env, ancestry = [] } = {}) {
2259
+ for (const name of ancestry) {
2260
+ const base = path.basename(String(name || ""));
2261
+ for (const [pattern, id] of TERMINAL_PROCESSES) {
2262
+ if (pattern.test(base)) return id;
2263
+ }
2264
+ }
2265
+ const program = env.TERM_PROGRAM || "";
2266
+ if (program === "vscode") return "vscode";
2267
+ if (program === "iTerm.app" || env.ITERM_SESSION_ID) return "iterm2";
2268
+ if (program === "Apple_Terminal") return "apple-terminal";
2269
+ if (program === "WezTerm" || env.WEZTERM_EXECUTABLE) return "wezterm";
2270
+ if (program === "ghostty" || env.GHOSTTY_RESOURCES_DIR) return "ghostty";
2271
+ if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
2272
+ if (env.ALACRITTY_SOCKET || env.ALACRITTY_WINDOW_ID || env.ALACRITTY_LOG) return "alacritty";
2273
+ if (/^foot/.test(env.TERM || "")) return "foot";
2274
+ if (env.KONSOLE_VERSION || env.KONSOLE_DBUS_SESSION) return "konsole";
2275
+ if (env.PTYXIS_VERSION) return "ptyxis";
2276
+ if (env.TILIX_ID) return "tilix";
2277
+ if (env.GNOME_TERMINAL_SCREEN || env.GNOME_TERMINAL_SERVICE) return "gnome-terminal";
2278
+ if (/^rxvt-unicode/.test(env.TERM || "")) return "urxvt";
2279
+ if (env.XTERM_VERSION) return "xterm";
2280
+ return null;
2281
+ }
2282
+ function processLineage({ platform = process.platform, pid = process.ppid, run = defaultRun } = {}) {
2283
+ const lineage = [];
2284
+ if (platform === "linux") {
2285
+ let current = pid;
2286
+ for (let depth = 0; depth < 32 && current > 1; depth += 1) {
2287
+ let stat;
2288
+ try {
2289
+ stat = fs.readFileSync(`/proc/${current}/stat`, "utf8");
2290
+ } catch {
2291
+ break;
2292
+ }
2293
+ const open = stat.indexOf("(");
2294
+ const close = stat.lastIndexOf(")");
2295
+ if (open === -1 || close === -1) break;
2296
+ lineage.push({ pid: current, name: stat.slice(open + 1, close) });
2297
+ current = Number(stat.slice(close + 2).split(" ")[1]);
2298
+ }
2299
+ return lineage;
2300
+ }
2301
+ if (platform === "darwin") {
2302
+ const result = run("ps", ["-axo", "pid=,ppid=,comm="]);
2303
+ if (result.status !== 0) return lineage;
2304
+ const table = /* @__PURE__ */ new Map();
2305
+ for (const line of result.stdout.split("\n")) {
2306
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
2307
+ if (match) table.set(Number(match[1]), { ppid: Number(match[2]), comm: match[3].trim() });
2308
+ }
2309
+ let current = pid;
2310
+ for (let depth = 0; depth < 32 && table.has(current) && current > 1; depth += 1) {
2311
+ const entry = table.get(current);
2312
+ lineage.push({ pid: current, name: entry.comm });
2313
+ current = entry.ppid;
2314
+ }
2315
+ }
2316
+ return lineage;
2317
+ }
2318
+ function processAncestry(options = {}) {
2319
+ return processLineage(options).map((entry) => entry.name);
2320
+ }
2321
+ function processArguments(pid, { platform = process.platform } = {}) {
2322
+ if (platform !== "linux") return [];
2323
+ try {
2324
+ return fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean);
2325
+ } catch {
2326
+ return [];
2327
+ }
2328
+ }
2329
+ function terminalProcess(source, lineage) {
2330
+ return lineage.find((entry) => TERMINAL_PROCESSES.some(([pattern, id]) => id === source && pattern.test(path.basename(String(entry.name || ""))))) || null;
2331
+ }
2332
+ function readGSettingsProfileFont(deps, { schema, path: profilePath, fontKey = "font" }) {
2333
+ const useSystem = gsettingsGet(deps, `${schema}:${profilePath}`, "use-system-font");
2334
+ if (useSystem === null) return null;
2335
+ if (useSystem === "false") {
2336
+ const font = parsePangoFontDescription(parseGVariantString(gsettingsGet(deps, `${schema}:${profilePath}`, fontKey)));
2337
+ if (font) return font;
2338
+ }
2339
+ return systemMonospaceFont(deps);
2340
+ }
2341
+ function systemMonospaceFont(deps) {
2342
+ return parsePangoFontDescription(parseGVariantString(
2343
+ gsettingsGet(deps, "org.gnome.desktop.interface", "monospace-font-name")
2344
+ ));
2345
+ }
2346
+ var READERS = {
2347
+ "gnome-terminal": (deps) => {
2348
+ const id = parseGVariantString(gsettingsGet(deps, "org.gnome.Terminal.ProfilesList", "default"));
2349
+ if (!id) return null;
2350
+ return readGSettingsProfileFont(deps, {
2351
+ schema: "org.gnome.Terminal.Legacy.Profile",
2352
+ path: `/org/gnome/terminal/legacy/profiles:/:${id}/`
2353
+ });
2354
+ },
2355
+ tilix: (deps) => {
2356
+ const id = parseGVariantString(gsettingsGet(deps, "com.gexperts.Tilix.ProfilesList", "default"));
2357
+ if (!id) return null;
2358
+ return readGSettingsProfileFont(deps, {
2359
+ schema: "com.gexperts.Tilix.Profile",
2360
+ path: `/com/gexperts/Tilix/profiles/${id}/`
2361
+ });
2362
+ },
2363
+ ptyxis: (deps) => {
2364
+ const useSystem = gsettingsGet(deps, "org.gnome.Ptyxis", "use-system-font");
2365
+ if (useSystem === "false") {
2366
+ const font = parsePangoFontDescription(parseGVariantString(gsettingsGet(deps, "org.gnome.Ptyxis", "font-name")));
2367
+ if (font) return font;
2368
+ }
2369
+ return useSystem === null ? null : systemMonospaceFont(deps);
2370
+ },
2371
+ konsole: (deps) => {
2372
+ const rc = parseIni(deps.readFile(path.join(configHome(deps), "konsolerc")));
2373
+ const profileName = rc["Desktop Entry"]?.DefaultProfile;
2374
+ if (profileName) {
2375
+ const profile = parseIni(deps.readFile(path.join(dataHome(deps), "konsole", profileName)));
2376
+ const font = parseQtFontString(profile.Appearance?.Font);
2377
+ if (font) return font;
2378
+ }
2379
+ const globals = parseIni(deps.readFile(path.join(configHome(deps), "kdeglobals")));
2380
+ return parseQtFontString(globals.General?.fixed) || { family: "Hack", sizePx: pointsToPx(10) };
2381
+ },
2382
+ "xfce4-terminal": (deps) => {
2383
+ const query = (property) => {
2384
+ const result2 = deps.run("xfconf-query", ["-c", "xfce4-terminal", "-p", property]);
2385
+ return result2.status === 0 ? result2.stdout.trim() : null;
2386
+ };
2387
+ const rc = parseIni(deps.readFile(path.join(configHome(deps), "xfce4", "terminal", "terminalrc"))).Configuration || {};
2388
+ const useSystem = (query("/font-use-system") ?? rc.FontUseSystem ?? "false").toLowerCase() === "true";
2389
+ if (!useSystem) {
2390
+ const font = parsePangoFontDescription(query("/font-name") ?? rc.FontName ?? "Monospace 12");
2391
+ if (font) return font;
2392
+ }
2393
+ const result = deps.run("xfconf-query", ["-c", "xsettings", "-p", "/Gtk/MonospaceFontName"]);
2394
+ return parsePangoFontDescription(result.status === 0 ? result.stdout.trim() : "Monospace 10");
2395
+ },
2396
+ kitty: (deps) => {
2397
+ const directory = deps.env.KITTY_CONFIG_DIRECTORY || path.join(configHome(deps), "kitty");
2398
+ const settings = {};
2399
+ const visit = (file, depth) => {
2400
+ const text = deps.readFile(file);
2401
+ if (text === null || depth > 4) return;
2402
+ for (const rawLine of text.split(/\r?\n/)) {
2403
+ const line = rawLine.trim();
2404
+ if (!line || line.startsWith("#")) continue;
2405
+ const [key, ...rest] = line.split(/\s+/);
2406
+ const value = rest.join(" ");
2407
+ if (key === "include") visit(path.resolve(directory, expandHome(value, deps)), depth + 1);
2408
+ else if (key === "font_family" || key === "font_size") settings[key] = value;
2409
+ }
2410
+ };
2411
+ visit(path.join(directory, "kitty.conf"), 0);
2412
+ const raw = settings.font_family || "monospace";
2413
+ const quoted = /family\s*=\s*(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(raw);
2414
+ const family = quoted ? quoted[1] || quoted[2] || quoted[3] : raw;
2415
+ return { family, sizePx: pointsToPx(settings.font_size || 11) };
2416
+ },
2417
+ alacritty: (deps) => {
2418
+ const candidates = [
2419
+ path.join(configHome(deps), "alacritty", "alacritty.toml"),
2420
+ path.join(configHome(deps), "alacritty.toml"),
2421
+ path.join(deps.home, ".alacritty.toml")
2422
+ ];
2423
+ const merged = {};
2424
+ const visit = (file, depth) => {
2425
+ const text = deps.readFile(file);
2426
+ if (text === null || depth > 4) return false;
2427
+ const values = parseTomlSubset(text);
2428
+ for (const imported of [...values.import || [], ...values["general.import"] || []]) {
2429
+ visit(path.resolve(path.dirname(file), expandHome(String(imported), deps)), depth + 1);
2430
+ }
2431
+ Object.assign(merged, values);
2432
+ return true;
2433
+ };
2434
+ candidates.some((file) => visit(file, 0));
2435
+ const fallbackFamily = deps.platform === "darwin" ? "Menlo" : "monospace";
2436
+ return {
2437
+ family: String(merged["font.normal.family"] || fallbackFamily),
2438
+ sizePx: pointsToPx(merged["font.size"] || 11.25)
2439
+ };
2440
+ },
2441
+ ghostty: (deps) => {
2442
+ const settings = { families: [], size: void 0 };
2443
+ const visit = (file, depth) => {
2444
+ const text = deps.readFile(file);
2445
+ if (text === null || depth > 4) return;
2446
+ for (const rawLine of text.split(/\r?\n/)) {
2447
+ const line = rawLine.trim();
2448
+ if (!line || line.startsWith("#")) continue;
2449
+ const equals = line.indexOf("=");
2450
+ if (equals === -1) continue;
2451
+ const key = line.slice(0, equals).trim();
2452
+ const value = line.slice(equals + 1).trim().replace(/^"(.*)"$/, "$1");
2453
+ if (key === "font-family") {
2454
+ if (value) settings.families.push(value);
2455
+ else settings.families = [];
2456
+ } else if (key === "font-size") {
2457
+ settings.size = value;
2458
+ } else if (key === "config-file") {
2459
+ const target = expandHome(value.replace(/^\?/, ""), deps);
2460
+ visit(path.resolve(path.dirname(file), target), depth + 1);
2461
+ }
2462
+ }
2463
+ };
2464
+ visit(path.join(configHome(deps), "ghostty", "config"), 0);
2465
+ if (deps.platform === "darwin") {
2466
+ visit(path.join(deps.home, "Library", "Application Support", "com.mitchellh.ghostty", "config"), 0);
2467
+ }
2468
+ return {
2469
+ family: settings.families[0] || "JetBrains Mono",
2470
+ sizePx: pointsToPx(settings.size || (deps.platform === "darwin" ? 13 : 12))
2471
+ };
2472
+ },
2473
+ wezterm: (deps) => {
2474
+ const result = deps.run("wezterm", ["ls-fonts"]);
2475
+ const listing = result.status === 0 ? result.stdout : "";
2476
+ const match = /family\s*=\s*"([^"]+)"/.exec(listing) || /font_with_fallback\(\{[\s\S]*?"([^"]+)"/.exec(listing);
2477
+ let sizePt = 12;
2478
+ for (const file of [path.join(deps.home, ".wezterm.lua"), path.join(configHome(deps), "wezterm", "wezterm.lua")]) {
2479
+ const size = /font_size\s*=\s*(\d+(?:\.\d+)?)/.exec(deps.readFile(file) || "");
2480
+ if (size) {
2481
+ sizePt = Number(size[1]);
2482
+ break;
2483
+ }
2484
+ }
2485
+ return { family: match ? match[1] : "JetBrains Mono", sizePx: pointsToPx(sizePt) };
2486
+ },
2487
+ foot: (deps) => {
2488
+ const ini = parseIni(deps.readFile(path.join(configHome(deps), "foot", "foot.ini")));
2489
+ const value = ini.main?.font || ini[""]?.font || "monospace:size=8";
2490
+ return parseFontconfigPattern(value);
2491
+ },
2492
+ vscode: (deps) => {
2493
+ const base = deps.platform === "darwin" ? path.join(deps.home, "Library", "Application Support") : configHome(deps);
2494
+ for (const product of ["Code", "Cursor", "Code - Insiders", "VSCodium", "Windsurf"]) {
2495
+ const settings = parseJsonc(deps.readFile(path.join(base, product, "User", "settings.json")));
2496
+ if (!settings) continue;
2497
+ const family = firstCssFamily(settings["terminal.integrated.fontFamily"]) || firstCssFamily(settings["editor.fontFamily"]);
2498
+ const sizePx = positiveNumber(settings["terminal.integrated.fontSize"]) || positiveNumber(settings["editor.fontSize"]) || 14;
2499
+ if (family) return { family, sizePx };
2500
+ }
2501
+ return { family: deps.platform === "darwin" ? "Menlo" : "Droid Sans Mono", sizePx: 14 };
2502
+ },
2503
+ iterm2: (deps) => {
2504
+ const prefs = readPreferences("com.googlecode.iterm2", deps);
2505
+ const profiles = Array.isArray(prefs?.["New Bookmarks"]) ? prefs["New Bookmarks"] : [];
2506
+ const profile = profiles.find((candidate) => candidate?.Guid === prefs["Default Bookmark Guid"]) || profiles[0];
2507
+ const match = /^(.+?)\s+(\d+(?:\.\d+)?)$/.exec(String(profile?.["Normal Font"] || "").trim());
2508
+ if (!match) return null;
2509
+ const family = familyForPostScriptName(match[1], deps);
2510
+ return family ? { family, sizePx: pointsToPx(match[2]) } : null;
2511
+ },
2512
+ "apple-terminal": (deps) => {
2513
+ const prefs = readPreferences("com.apple.Terminal", deps);
2514
+ if (!prefs) return null;
2515
+ const name = prefs["Default Window Settings"] || prefs["Startup Window Settings"] || "Basic";
2516
+ const profile = prefs["Window Settings"]?.[name];
2517
+ let postscript = "SFMono-Regular";
2518
+ let points = 11;
2519
+ if (Buffer.isBuffer(profile?.Font)) {
2520
+ const font = unarchiveKeyed(parseBinaryPlist(profile.Font));
2521
+ if (typeof font?.NSName === "string" && font.NSName) postscript = font.NSName;
2522
+ if (Number(font?.NSSize) > 0) points = Number(font.NSSize);
2523
+ }
2524
+ const family = familyForPostScriptName(postscript, deps);
2525
+ return family ? { family, sizePx: pointsToPx(points) } : null;
2526
+ },
2527
+ xterm: (deps) => {
2528
+ const classes = ["XTerm", "xterm", "UXTerm", "uxterm"];
2529
+ const args = deps.terminalArgs || [];
2530
+ const resources = [...xResources(deps), ...xrmResources(args)];
2531
+ const face = optionValue(args, ["-fa", "-faceName"]) || xResource(resources, classes, "faceName");
2532
+ if (!face) return null;
2533
+ const font = parseXFontName(face, { bareIsPattern: true });
2534
+ if (!font) return null;
2535
+ const size = Number(optionValue(args, ["-fs", "-faceSize"]) || xResource(resources, classes, "faceSize"));
2536
+ return size > 0 ? { ...font, sizePx: pointsToPx(size) } : font;
2537
+ },
2538
+ urxvt: (deps) => {
2539
+ const args = deps.terminalArgs || [];
2540
+ const resources = [...xResources(deps), ...xrmResources(args)];
2541
+ const font = optionValue(args, ["-fn"]) || xResource(resources, ["URxvt", "urxvt", "Rxvt", "rxvt"], "font");
2542
+ return font ? parseXFontName(font) : null;
2543
+ }
2544
+ };
2545
+ function readPreferences(domain, deps) {
2546
+ const data = deps.readBuffer(path.join(deps.home, "Library", "Preferences", `${domain}.plist`));
2547
+ if (!data) return null;
2548
+ try {
2549
+ return parseBinaryPlist(data);
2550
+ } catch {
2551
+ return null;
2552
+ }
2553
+ }
2554
+ function optionValue(args, names) {
2555
+ let value = null;
2556
+ for (let i = 0; i < args.length - 1; i += 1) {
2557
+ if (names.includes(args[i])) value = args[i + 1];
2558
+ }
2559
+ return value;
2560
+ }
2561
+ function xrmResources(args) {
2562
+ const entries = [];
2563
+ for (let i = 0; i < args.length - 1; i += 1) {
2564
+ if (args[i] !== "-xrm") continue;
2565
+ const colon = args[i + 1].indexOf(":");
2566
+ if (colon !== -1) entries.push([args[i + 1].slice(0, colon).trim(), args[i + 1].slice(colon + 1).trim()]);
2567
+ }
2568
+ return entries;
2569
+ }
2570
+ function xResources(deps) {
2571
+ const result = deps.run("xrdb", ["-query"]);
2572
+ if (result.status !== 0) return [];
2573
+ return result.stdout.split("\n").map((line) => {
2574
+ const colon = line.indexOf(":");
2575
+ return colon === -1 ? null : [line.slice(0, colon).trim(), line.slice(colon + 1).trim()];
2576
+ }).filter(Boolean);
2577
+ }
2578
+ function xResource(resources, classes, resource) {
2579
+ let named = null;
2580
+ let wildcard = null;
2581
+ for (const [name, value] of resources) {
2582
+ const parts = name.split(/[.*]/).filter(Boolean);
2583
+ if (parts[parts.length - 1] !== resource || !value) continue;
2584
+ if (parts.length === 1) wildcard = value;
2585
+ else if (classes.includes(parts[0])) named = value;
2586
+ }
2587
+ return named ?? wildcard;
2588
+ }
2589
+ function parseXFontName(value, { bareIsPattern = false } = {}) {
2590
+ const first = String(value || "").split(/,(?=\s*(?:xft:|-))/)[0].trim();
2591
+ if (/^xft:/i.test(first)) return parseFontconfigPattern(first.slice(4));
2592
+ if (first.startsWith("-")) {
2593
+ const fields = first.split("-");
2594
+ const family = (fields[2] || "").trim();
2595
+ if (!family || family === "*" || /^(fixed|misc)$/i.test(family)) return null;
2596
+ const pixels = Number(fields[7]);
2597
+ const decipoints = Number(fields[8]);
2598
+ return { family, sizePx: pixels > 0 ? pixels : decipoints > 0 ? pointsToPx(decipoints / 10) : void 0 };
2599
+ }
2600
+ if (!first) return null;
2601
+ return bareIsPattern || first.includes(":") ? parseFontconfigPattern(first) : null;
2602
+ }
2603
+ var FACE_PATTERNS = {
2604
+ regular: ":weight=regular:slant=roman",
2605
+ bold: ":weight=bold:slant=roman",
2606
+ italic: ":weight=regular:slant=italic",
2607
+ boldItalic: ":weight=bold:slant=italic"
2608
+ };
2609
+ function escapeFontconfig(value) {
2610
+ return value.replace(/([\\\-:,])/g, "\\$1");
2611
+ }
2612
+ function sfntFormat(header) {
2613
+ if (!header || header.length < 4) return null;
2614
+ const tag = header.subarray(0, 4).toString("latin1");
2615
+ if (tag === "\0\0\0" || tag === "true") return "truetype";
2616
+ if (tag === "OTTO") return "opentype";
2617
+ return null;
2618
+ }
2619
+ function readHeader(filePath, length) {
2620
+ let fd;
2621
+ try {
2622
+ fd = fs.openSync(filePath, "r");
2623
+ const buffer = Buffer.alloc(length);
2624
+ const read = fs.readSync(fd, buffer, 0, length, 0);
2625
+ return buffer.subarray(0, read);
2626
+ } catch {
2627
+ return null;
2628
+ } finally {
2629
+ if (fd !== void 0) try {
2630
+ fs.closeSync(fd);
2631
+ } catch {
2632
+ }
2633
+ }
2634
+ }
2635
+ function readSfntNames(filePath) {
2636
+ let fd;
2637
+ try {
2638
+ fd = fs.openSync(filePath, "r");
2639
+ const head = Buffer.alloc(12);
2640
+ fs.readSync(fd, head, 0, 12, 0);
2641
+ if (!sfntFormat(head)) return null;
2642
+ const tables = head.readUInt16BE(4);
2643
+ const directory = Buffer.alloc(tables * 16);
2644
+ fs.readSync(fd, directory, 0, directory.length, 12);
2645
+ let offset = -1;
2646
+ let length = 0;
2647
+ for (let i = 0; i < tables; i += 1) {
2648
+ if (directory.toString("latin1", i * 16, i * 16 + 4) === "name") {
2649
+ offset = directory.readUInt32BE(i * 16 + 8);
2650
+ length = directory.readUInt32BE(i * 16 + 12);
2651
+ }
2652
+ }
2653
+ if (offset < 0 || length > 1024 * 1024) return null;
2654
+ const table = Buffer.alloc(length);
2655
+ fs.readSync(fd, table, 0, length, offset);
2656
+ const count = table.readUInt16BE(2);
2657
+ const strings = table.readUInt16BE(4);
2658
+ const names = {};
2659
+ for (let i = 0; i < count; i += 1) {
2660
+ const record = 6 + i * 12;
2661
+ const platformId = table.readUInt16BE(record);
2662
+ const languageId = table.readUInt16BE(record + 4);
2663
+ const nameId = table.readUInt16BE(record + 6);
2664
+ const size = table.readUInt16BE(record + 8);
2665
+ const start = strings + table.readUInt16BE(record + 10);
2666
+ if (![1, 2, 6, 16, 17].includes(nameId) || start + size > table.length) continue;
2667
+ let text;
2668
+ if ((platformId === 3 || platformId === 0) && size % 2 === 0) {
2669
+ if (platformId === 3 && languageId !== 1033 && names[nameId]) continue;
2670
+ const utf16 = Buffer.from(table.subarray(start, start + size));
2671
+ text = utf16.swap16().toString("utf16le");
2672
+ } else if (platformId === 1 && !names[nameId]) {
2673
+ text = table.toString("latin1", start, start + size);
2674
+ }
2675
+ if (text) names[nameId] = text;
2676
+ }
2677
+ return {
2678
+ family: names[16] || names[1] || null,
2679
+ subfamily: names[17] || names[2] || "",
2680
+ postscript: names[6] || null
2681
+ };
2682
+ } catch {
2683
+ return null;
2684
+ } finally {
2685
+ if (fd !== void 0) try {
2686
+ fs.closeSync(fd);
2687
+ } catch {
2688
+ }
2689
+ }
2690
+ }
2691
+ function macFontFiles(deps) {
2692
+ const roots = [
2693
+ path.join(deps.home, "Library", "Fonts"),
2694
+ "/Library/Fonts",
2695
+ "/System/Library/Fonts"
2696
+ ];
2697
+ const files = [];
2698
+ const walk = (directory, depth) => {
2699
+ let entries;
2700
+ try {
2701
+ entries = fs.readdirSync(directory, { withFileTypes: true });
2702
+ } catch {
2703
+ return;
2704
+ }
2705
+ for (const entry of entries) {
2706
+ const full = path.join(directory, entry.name);
2707
+ if (entry.isDirectory() && depth < 2) walk(full, depth + 1);
2708
+ else if (/\.(ttf|otf)$/i.test(entry.name)) files.push(full);
2709
+ }
2710
+ };
2711
+ for (const root of roots) walk(root, 0);
2712
+ return files;
2713
+ }
2714
+ var APPLE_POSTSCRIPT_FAMILIES = [
2715
+ [/^\.?SF(NS)?Mono/, "SF Mono"],
2716
+ [/^Menlo-/, "Menlo"],
2717
+ [/^Monaco$/, "Monaco"],
2718
+ [/^Courier(New)?/, "Courier New"],
2719
+ [/^AndaleMono$/, "Andale Mono"]
2720
+ ];
2721
+ function familyForPostScriptName(postscript, deps) {
2722
+ const apple = APPLE_POSTSCRIPT_FAMILIES.find(([pattern]) => pattern.test(postscript));
2723
+ if (apple && deps.platform === "darwin") return apple[1];
2724
+ const listed = deps.run("fc-list", [`:postscriptname=${escapeFontconfig(postscript)}`, "family"]);
2725
+ if (listed.status === 0 && listed.stdout.trim()) return listed.stdout.trim().split("\n")[0].split(",")[0].trim();
2726
+ if (deps.platform !== "darwin") return null;
2727
+ for (const file of macFontFiles(deps)) {
2728
+ const names = readSfntNames(file);
2729
+ if (names?.postscript === postscript) return names.family;
2730
+ }
2731
+ return null;
2732
+ }
2733
+ function styleOfSubfamily(subfamily) {
2734
+ const text = String(subfamily || "").toLowerCase();
2735
+ const bold = /\bbold\b/.test(text) && !/(semi|demi|extra|ultra)\s*bold/.test(text);
2736
+ const italic = /italic|oblique/.test(text);
2737
+ if (bold && italic) return "boldItalic";
2738
+ if (bold) return "bold";
2739
+ if (italic) return "italic";
2740
+ return /^(regular|normal|book|roman)$/.test(text.trim()) ? "regular" : null;
2741
+ }
2742
+ function locateFaceFiles(family, deps) {
2743
+ const located = {};
2744
+ let fontconfig = false;
2745
+ for (const style of TERMINAL_FONT_STYLES) {
2746
+ const result = deps.run("fc-match", ["-f", "%{file}\n%{family}\n%{index}\n", `${escapeFontconfig(family)}${FACE_PATTERNS[style]}`]);
2747
+ if (result.status !== 0) continue;
2748
+ fontconfig = true;
2749
+ const [file, families = "", index = "0"] = result.stdout.split("\n");
2750
+ const names = families.split(",").map((name) => name.trim().toLowerCase());
2751
+ if (!file || !names.includes(family.toLowerCase()) || Number(index) !== 0) continue;
2752
+ located[style] = file;
2753
+ }
2754
+ if (fontconfig || deps.platform !== "darwin") return located;
2755
+ for (const file of macFontFiles(deps)) {
2756
+ const names = readSfntNames(file);
2757
+ if (!names || names.family?.toLowerCase() !== family.toLowerCase()) continue;
2758
+ const style = styleOfSubfamily(names.subfamily);
2759
+ if (style && !located[style]) located[style] = file;
2760
+ }
2761
+ return located;
2762
+ }
2763
+ function hashFile(filePath) {
2764
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
2765
+ }
2766
+ function resolveFontFaces(family, deps = {}) {
2767
+ const resolved = withDefaults(deps);
2768
+ if (!family || GENERIC_FAMILIES.has(family.toLowerCase())) return [];
2769
+ const located = locateFaceFiles(family, resolved);
2770
+ const faces = [];
2771
+ const usedFiles = /* @__PURE__ */ new Set();
2772
+ for (const style of TERMINAL_FONT_STYLES) {
2773
+ const file = located[style];
2774
+ if (!file || usedFiles.has(file)) continue;
2775
+ if (/^\/System\//.test(file) || /\.app\/Contents\//.test(file)) continue;
2776
+ if (style !== "regular" && !faces.length) continue;
2777
+ let stat;
2778
+ try {
2779
+ stat = fs.statSync(file);
2780
+ } catch {
2781
+ continue;
2782
+ }
2783
+ if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_TERMINAL_FONT_BYTES) continue;
2784
+ const format = sfntFormat(readHeader(file, 4));
2785
+ if (!format) continue;
2786
+ usedFiles.add(file);
2787
+ faces.push({ style, format, bytes: stat.size, sha256: hashFile(file), path: file, mtimeMs: stat.mtimeMs });
2788
+ }
2789
+ return faces;
2790
+ }
2791
+ var MAX_SUBSET_SOURCE_BYTES = 64 * 1024 * 1024;
2792
+ function locateFont(pattern, deps) {
2793
+ const result = deps.run("fc-match", ["-f", "%{file}\n%{family}\n%{index}\n", pattern]);
2794
+ if (result.status !== 0) return null;
2795
+ const [file, families = "", index = "0"] = result.stdout.split("\n");
2796
+ if (!file) return null;
2797
+ return { file, families: families.split(",").map((name) => name.trim()).filter(Boolean), index: Number(index) || 0 };
2798
+ }
2799
+ function resolveSubsetSources(family, faces, deps = {}) {
2800
+ const resolved = withDefaults(deps);
2801
+ if (!family || GENERIC_FAMILIES.has(family.toLowerCase())) return [];
2802
+ const wanted = family.toLowerCase();
2803
+ const sources = [];
2804
+ const add = (located, scope, name) => {
2805
+ if (!located || /^\/System\//.test(located.file) || /\.app\/Contents\//.test(located.file)) return;
2806
+ let stat;
2807
+ try {
2808
+ stat = fs.statSync(located.file);
2809
+ } catch {
2810
+ return;
2811
+ }
2812
+ if (!stat.isFile() || stat.size > MAX_SUBSET_SOURCE_BYTES) return;
2813
+ const header = readHeader(located.file, 4);
2814
+ if (!sfntFormat(header) && header?.toString("latin1") !== "ttcf") return;
2815
+ const sha256 = crypto.createHash("sha256").update(`${located.file}\0${located.index}\0${stat.size}\0${stat.mtimeMs}`).digest("hex");
2816
+ sources.push({
2817
+ family: name,
2818
+ style: "regular",
2819
+ scope,
2820
+ sha256,
2821
+ path: located.file,
2822
+ index: located.index,
2823
+ bytes: stat.size,
2824
+ mtimeMs: stat.mtimeMs
2825
+ });
2826
+ };
2827
+ const self = locateFont(`${escapeFontconfig(family)}${FACE_PATTERNS.regular}`, resolved);
2828
+ const selfInstalled = self && self.families.some((name) => name.toLowerCase() === wanted);
2829
+ if (selfInstalled && !faces.some((face) => face.style === "regular")) add(self, "all", family);
2830
+ const cjk = locateFont(`${escapeFontconfig(family)}:charset=4e00${FACE_PATTERNS.regular}`, resolved);
2831
+ const coveredBySelf = cjk && cjk.families.some((name) => name.toLowerCase() === wanted);
2832
+ if (cjk && !coveredBySelf && cjk.families[0]) add(cjk, "cjk", cjk.families[0]);
2833
+ return sources;
2834
+ }
2835
+ function concreteFamily(family, deps) {
2836
+ if (!GENERIC_FAMILIES.has(String(family).toLowerCase())) return family;
2837
+ const result = deps.run("fc-match", ["-f", "%{family[0]}", family]);
2838
+ const matched = result.status === 0 ? result.stdout.trim() : "";
2839
+ return matched || null;
2840
+ }
2841
+ function readTerminalFont(source, deps = {}) {
2842
+ const resolved = withDefaults(deps);
2843
+ const reader = READERS[source];
2844
+ if (!reader) return null;
2845
+ let font;
2846
+ try {
2847
+ font = reader(resolved);
2848
+ } catch {
2849
+ return null;
2850
+ }
2851
+ if (!font?.family) return null;
2852
+ const family = concreteFamily(font.family, resolved);
2853
+ if (!family) return null;
2854
+ return sanitizeDetected({ source, family, sizePx: font.sizePx });
2855
+ }
2856
+ function detectTerminalFont({ env = process.env, ancestry, lineage, deps = {} } = {}) {
2857
+ const processes = lineage || (ancestry ? ancestry.map((name) => ({ pid: 0, name })) : processLineage({ run: deps.run }));
2858
+ const source = identifyTerminal({ env, ancestry: processes.map((entry) => entry.name) });
2859
+ if (!source) return null;
2860
+ const emulator = terminalProcess(source, processes);
2861
+ const terminalArgs = deps.terminalArgs || (emulator?.pid ? processArguments(emulator.pid) : []);
2862
+ return readTerminalFont(source, { ...deps, env, terminalArgs });
2863
+ }
2864
+ function sanitizeDetected(value) {
2865
+ const clean = sanitizeTerminalFont(value);
2866
+ if (!clean) return null;
2867
+ const detected = { source: clean.source || null, family: clean.family };
2868
+ if (clean.sizePx) detected.sizePx = clean.sizePx;
2869
+ return detected;
2870
+ }
2871
+ function describeTerminalFont(detected, deps = {}) {
2872
+ if (!detected?.family) return null;
2873
+ const faces = resolveFontFaces(detected.family, deps);
2874
+ return { ...detected, faces, subsets: resolveSubsetSources(detected.family, faces, deps) };
2875
+ }
2876
+ function publicTerminalFont(record) {
2877
+ return record ? sanitizeTerminalFont(record) : null;
2878
+ }
2879
+ function readFontChunk(record, sha256, index) {
2880
+ const face = record?.faces?.find((candidate) => candidate.sha256 === sha256);
2881
+ if (!face) return null;
2882
+ const total = Math.ceil(face.bytes / TERMINAL_FONT_CHUNK_BYTES);
2883
+ if (!Number.isInteger(index) || index < 0 || index >= total) return null;
2884
+ let fd;
2885
+ try {
2886
+ const stat = fs.statSync(face.path);
2887
+ if (stat.size !== face.bytes || stat.mtimeMs !== face.mtimeMs) return null;
2888
+ fd = fs.openSync(face.path, "r");
2889
+ const start = index * TERMINAL_FONT_CHUNK_BYTES;
2890
+ const length = Math.min(TERMINAL_FONT_CHUNK_BYTES, face.bytes - start);
2891
+ const buffer = Buffer.alloc(length);
2892
+ const read = fs.readSync(fd, buffer, 0, length, start);
2893
+ if (read !== length) return null;
2894
+ return { data: buffer, total };
2895
+ } catch {
2896
+ return null;
2897
+ } finally {
2898
+ if (fd !== void 0) try {
2899
+ fs.closeSync(fd);
2900
+ } catch {
2901
+ }
2902
+ }
2903
+ }
2904
+ function fontFromEnvironment(env = process.env) {
2905
+ const raw = env.HERDR_TERM_FONT_JSON;
2906
+ if (!raw) return null;
2907
+ try {
2908
+ return sanitizeDetected(JSON.parse(raw));
2909
+ } catch {
2910
+ return null;
2911
+ }
2912
+ }
2913
+ function rememberTerminalFont(detected) {
2914
+ const clean = sanitizeDetected(detected);
2915
+ if (!clean) return null;
2916
+ try {
2917
+ ensureDir(stateDir2());
2918
+ const state = readJson(runtimeStatePath(), {});
2919
+ const previous = state.terminalFont;
2920
+ if (!previous || JSON.stringify(previous) !== JSON.stringify(clean)) {
2921
+ writeJsonAtomic(runtimeStatePath(), { ...state, terminalFont: clean });
2922
+ }
2923
+ } catch {
2924
+ }
2925
+ return clean;
2926
+ }
2927
+ function rememberedTerminalFont() {
2928
+ try {
2929
+ return sanitizeDetected(readJson(runtimeStatePath(), {}).terminalFont);
2930
+ } catch {
2931
+ return null;
2932
+ }
2933
+ }
2934
+ function captureTerminalFont({ env = process.env, detect = detectTerminalFont } = {}) {
2935
+ if (env.HERDR_TERM_FONT_JSON) return fontFromEnvironment(env);
2936
+ const detected = detect({ env });
2937
+ if (!detected) return null;
2938
+ env.HERDR_TERM_FONT_JSON = JSON.stringify(detected);
2939
+ rememberTerminalFont(detected);
2940
+ return detected;
2941
+ }
2942
+ function resolveHostFont({ env = process.env } = {}) {
2943
+ return fontFromEnvironment(env) || rememberedTerminalFont();
2944
+ }
2945
+ function loadHostTerminalFont({ env = process.env, refresh = false, deps = {} } = {}) {
2946
+ const known = resolveHostFont({ env });
2947
+ let current = known?.source ? readTerminalFont(known.source, { ...deps, env }) : null;
2948
+ if (!current && refresh && !known) current = detectTerminalFont({ env, deps });
2949
+ if (current && JSON.stringify(current) !== JSON.stringify(known)) rememberTerminalFont(current);
2950
+ return describeTerminalFont(current || known, deps);
2951
+ }
2952
+ module.exports = {
2953
+ PX_PER_PT,
2954
+ parsePangoFontDescription,
2955
+ parseQtFontString,
2956
+ parseFontconfigPattern,
2957
+ parseGVariantString,
2958
+ parseTomlSubset,
2959
+ parseJsonc,
2960
+ parseXFontName,
2961
+ xResource,
2962
+ firstCssFamily,
2963
+ identifyTerminal,
2964
+ processAncestry,
2965
+ processLineage,
2966
+ readTerminalFont,
2967
+ detectTerminalFont,
2968
+ resolveFontFaces,
2969
+ resolveSubsetSources,
2970
+ readSfntNames,
2971
+ sfntFormat,
2972
+ describeTerminalFont,
2973
+ publicTerminalFont,
2974
+ readFontChunk,
2975
+ fontFromEnvironment,
2976
+ rememberTerminalFont,
2977
+ rememberedTerminalFont,
2978
+ captureTerminalFont,
2979
+ resolveHostFont,
2980
+ loadHostTerminalFont
2981
+ };
2982
+ }
2983
+ });
2984
+
1830
2985
  // src/socket-discovery.js
1831
2986
  var require_socket_discovery = __commonJS({
1832
2987
  "src/socket-discovery.js"(exports, module) {
@@ -1915,6 +3070,7 @@ var require_service = __commonJS({
1915
3070
  rememberTerminalPalette,
1916
3071
  rememberedTerminalPalette
1917
3072
  } = require_terminal_palette();
3073
+ var { resolveHostFont } = require_terminal_font();
1918
3074
  var { resolveSocketPath } = require_socket_discovery();
1919
3075
  var { preferredLanAddress: preferredLanAddress2 } = require_net_interfaces();
1920
3076
  var { MIN_HERDR_VERSION: MIN_HERDR_VERSION2, findHerdrCommand, herdrVersion: herdrVersion2, resolveHerdrCommand } = require_herdr_command();
@@ -1971,6 +3127,9 @@ var require_service = __commonJS({
1971
3127
  cachedTerminalPalette = rememberedTerminalPalette();
1972
3128
  return cachedTerminalPalette;
1973
3129
  }
3130
+ function hostTerminalFont() {
3131
+ return resolveHostFont();
3132
+ }
1974
3133
  function logPath(name) {
1975
3134
  return path.join(stateDir2(), `${name}.log`);
1976
3135
  }
@@ -2012,6 +3171,7 @@ var require_service = __commonJS({
2012
3171
  });
2013
3172
  }
2014
3173
  const terminalPalette = hostTerminalPalette();
3174
+ const terminalFont = hostTerminalFont();
2015
3175
  specs.push({
2016
3176
  name: "host",
2017
3177
  command: process.execPath,
@@ -2020,6 +3180,7 @@ var require_service = __commonJS({
2020
3180
  // Captured here, where a terminal may still be attached, because the
2021
3181
  // connector itself usually runs detached with no terminal to ask.
2022
3182
  ...terminalPalette ? { HERDR_TERM_PALETTE_JSON: JSON.stringify(terminalPalette) } : {},
3183
+ ...terminalFont ? { HERDR_TERM_FONT_JSON: JSON.stringify(terminalFont) } : {},
2023
3184
  RELAY_URL: resolveHostRelayUrl(config),
2024
3185
  RELAY_HOST_ID: state.hostId,
2025
3186
  RELAY_HOST_TOKEN: state.hostToken,
@@ -2297,6 +3458,7 @@ var require_service = __commonJS({
2297
3458
  serviceSpecs,
2298
3459
  baseEnvironment,
2299
3460
  hostTerminalPalette,
3461
+ hostTerminalFont,
2300
3462
  startServices,
2301
3463
  stopServices,
2302
3464
  restartServices,
@@ -4068,7 +5230,7 @@ function About({ ctx }) {
4068
5230
  ),
4069
5231
  sourceNote ? /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: ` ${sourceNote}` }) : null
4070
5232
  ] }),
4071
- /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.17" }) }),
5233
+ /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.18" }) }),
4072
5234
  /* @__PURE__ */ jsx10(Row, { label: t("about.configPath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.configPath)() }) }),
4073
5235
  /* @__PURE__ */ jsx10(Row, { label: t("about.statePath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.stateDir)() }) }),
4074
5236
  /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.docs") }) }),