herdr-remote 0.2.16 → 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
@@ -415,6 +415,9 @@ var require_updater = __commonJS({
415
415
  }
416
416
  return "npm";
417
417
  }
418
+ function updateChecksEnabled2(env = process.env) {
419
+ return env.HERDR_REMOTE_UPDATE_CHECK !== "0";
420
+ }
418
421
  function canSelfUpdate2() {
419
422
  return installKind2() === "npm";
420
423
  }
@@ -466,92 +469,173 @@ var require_updater = __commonJS({
466
469
  }
467
470
  return 0;
468
471
  }
469
- async function askRegistry(registry, { timeoutMs, fetchImpl }) {
472
+ async function fetchJson(url, { timeoutMs, fetchImpl }) {
470
473
  const controller = new AbortController();
471
474
  const timer = setTimeout(() => controller.abort(), timeoutMs);
472
475
  try {
473
- const response = await fetchImpl(`${registry}/${PACKAGE_NAME}/latest`, {
476
+ const response = await fetchImpl(url, {
474
477
  signal: controller.signal,
475
- headers: { Accept: "application/vnd.npm.install-v1+json" }
478
+ headers: { Accept: "application/json", "Cache-Control": "no-cache" }
476
479
  });
477
480
  if (!response.ok) return { ok: false, message: `HTTP ${response.status ?? "?"}` };
478
- const body = await response.json();
479
- const latest = typeof body?.version === "string" ? body.version : null;
480
- if (!latest) return { ok: false, message: "registry answered without a version" };
481
- return { ok: true, latest };
481
+ return { ok: true, body: await response.json() };
482
482
  } catch (error) {
483
483
  return { ok: false, message: error?.name === "AbortError" ? "timed out" : String(error?.message || error) };
484
484
  } finally {
485
485
  clearTimeout(timer);
486
486
  }
487
487
  }
488
+ async function askRegistry(registry, { timeoutMs, fetchImpl }) {
489
+ const endpoints = [
490
+ { url: `${registry}/-/package/${PACKAGE_NAME}/dist-tags`, read: (body) => body?.latest },
491
+ { url: `${registry}/${PACKAGE_NAME}/latest`, read: (body) => body?.version }
492
+ ];
493
+ let message = null;
494
+ for (const endpoint of endpoints) {
495
+ const attempt = await fetchJson(endpoint.url, { timeoutMs, fetchImpl });
496
+ if (!attempt.ok) {
497
+ message = attempt.message;
498
+ continue;
499
+ }
500
+ const latest = endpoint.read(attempt.body);
501
+ if (typeof latest === "string" && latest) return { ok: true, latest };
502
+ message = "registry answered without a version";
503
+ }
504
+ return { ok: false, message };
505
+ }
488
506
  async function checkForUpdate2({
489
507
  timeoutMs = 6e3,
490
508
  fetchImpl = globalThis.fetch,
491
509
  registries = registryCandidates()
492
510
  } = {}) {
493
511
  const current = currentVersion();
494
- const failures = [];
495
- for (const registry of registries) {
496
- const attempt = await askRegistry(registry, { timeoutMs, fetchImpl });
497
- if (attempt.ok) {
498
- return {
499
- ok: true,
500
- current,
501
- latest: attempt.latest,
502
- registry,
503
- updateAvailable: compareVersions(attempt.latest, current) > 0
504
- };
505
- }
506
- failures.push(`${registry}: ${attempt.message}`);
512
+ const attempts = await Promise.all(registries.map(async (registry) => ({
513
+ registry,
514
+ ...await askRegistry(registry, { timeoutMs, fetchImpl })
515
+ })));
516
+ const answers = attempts.filter((attempt) => attempt.ok);
517
+ if (answers.length === 0) {
518
+ return {
519
+ ok: false,
520
+ current,
521
+ errorKey: "update.errorNetwork",
522
+ message: attempts.map((attempt) => `${attempt.registry}: ${attempt.message}`).join("; "),
523
+ triedRegistries: registries
524
+ };
507
525
  }
526
+ const latest = answers.map((answer) => answer.latest).reduce((best, version) => compareVersions(version, best) > 0 ? version : best);
527
+ const sources = answers.filter((answer) => compareVersions(answer.latest, latest) === 0).map((answer) => answer.registry);
528
+ const behind = answers.filter((answer) => compareVersions(answer.latest, latest) < 0).map((answer) => ({ registry: answer.registry, version: answer.latest }));
508
529
  return {
509
- ok: false,
530
+ ok: true,
510
531
  current,
511
- errorKey: "update.errorNetwork",
512
- message: failures.join("; "),
513
- triedRegistries: registries
532
+ latest,
533
+ registry: sources[0],
534
+ sources,
535
+ behind,
536
+ updateAvailable: compareVersions(latest, current) > 0
514
537
  };
515
538
  }
516
- function performUpdate2({
517
- spawnImpl = spawn,
518
- timeoutMs = 18e4,
519
- // Seam for tests: the suite runs from a checkout, where the guard below is
520
- // correctly the only reachable outcome.
521
- installKindImpl = installKind2,
522
- // The registry that just answered the version check. Installing from
523
- // somewhere the machine has been shown to reach beats installing from a
524
- // default it may have no route to. Empty means "whatever npm is configured
525
- // with", which is the right answer when no check has run.
526
- registry = ""
527
- } = {}) {
539
+ function npmErrorSummary(output) {
540
+ const lines = String(output || "").split(/\r?\n/).map((line) => line.trim()).filter((line) => /^npm (error|ERR!)/.test(line) && !/complete log of this run/i.test(line)).map((line) => line.replace(/^npm (error|ERR!)\s*/, "")).filter(Boolean);
541
+ return (lines.length > 0 ? lines.slice(0, 3) : String(output || "").trim().split(/\r?\n/).slice(-3)).join(" ");
542
+ }
543
+ function isNotYetPublished(output) {
544
+ return /\bETARGET\b|notarget|No matching version found/i.test(String(output || ""));
545
+ }
546
+ function installedVersionOnDisk() {
547
+ try {
548
+ return JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8")).version;
549
+ } catch {
550
+ return null;
551
+ }
552
+ }
553
+ function runNpm(spawnImpl, args, timeoutMs) {
528
554
  return new Promise((resolve) => {
529
- const kind = installKindImpl();
530
- if (kind !== "npm") {
531
- resolve({ ok: false, errorKey: `update.cannot.${kind}` });
555
+ let child;
556
+ try {
557
+ child = spawnImpl("npm", args, { timeout: timeoutMs });
558
+ } catch (error) {
559
+ resolve({ ok: false, spawnFailed: true, output: String(error?.message || error) });
532
560
  return;
533
561
  }
534
- const args = ["install", "-g", `${PACKAGE_NAME}@latest`];
535
- const normalizedRegistry = normalizeRegistry(registry);
536
- if (normalizedRegistry) args.push("--registry", normalizedRegistry);
537
- const child = spawnImpl("npm", args, {
538
- encoding: "utf8",
539
- timeout: timeoutMs
540
- });
541
562
  let output = "";
563
+ let settled = false;
564
+ const finish = (result) => {
565
+ if (!settled) {
566
+ settled = true;
567
+ resolve(result);
568
+ }
569
+ };
542
570
  const collect = (chunk) => {
543
571
  output += String(chunk);
544
572
  };
545
573
  child.stdout?.on("data", collect);
546
574
  child.stderr?.on("data", collect);
547
- child.on("error", (error) => {
548
- resolve({ ok: false, errorKey: "update.errorFailed", output: error.message });
549
- });
550
- child.on("close", (code) => {
551
- resolve({ ok: code === 0, output: output.trim().slice(-2e3) });
552
- });
575
+ child.on("error", (error) => finish({ ok: false, spawnFailed: true, output: error.message }));
576
+ child.on("close", (code) => finish({ ok: code === 0, output: output.trim() }));
553
577
  });
554
578
  }
579
+ var VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
580
+ async function performUpdate2({
581
+ spawnImpl = spawn,
582
+ timeoutMs = 18e4,
583
+ // Seam for tests: the suite runs from a checkout, where the guard below is
584
+ // correctly the only reachable outcome.
585
+ installKindImpl = installKind2,
586
+ // The registries that reported `version`, preferred first. Empty means
587
+ // "whatever npm is configured with", which is right when no check has run.
588
+ registry = "",
589
+ sources = [],
590
+ version = null,
591
+ attempts = 3,
592
+ retryDelayMs = 15e3,
593
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
594
+ readInstalledVersion = installedVersionOnDisk,
595
+ onAttempt = () => {
596
+ }
597
+ } = {}) {
598
+ const kind = installKindImpl();
599
+ if (kind !== "npm") return { ok: false, errorKey: `update.cannot.${kind}` };
600
+ const target = typeof version === "string" && VERSION_PATTERN.test(version) ? version : null;
601
+ const spec = `${PACKAGE_NAME}@${target || "latest"}`;
602
+ const registries = [];
603
+ for (const candidate of [registry, ...sources]) {
604
+ const normalized = normalizeRegistry(candidate);
605
+ if (normalized && !registries.includes(normalized)) registries.push(normalized);
606
+ }
607
+ if (registries.length === 0) registries.push("");
608
+ let last = { output: "" };
609
+ let notYetPublished = false;
610
+ for (const source of registries) {
611
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
612
+ onAttempt({ registry: source, attempt });
613
+ const args = ["install", "-g", spec, "--prefer-online"];
614
+ if (source) args.push("--registry", source);
615
+ const result = await runNpm(spawnImpl, args, timeoutMs);
616
+ if (result.ok) {
617
+ const installed = readInstalledVersion();
618
+ if (target && installed && compareVersions(installed, target) < 0) {
619
+ return { ok: false, errorKey: "update.errorNotApplied", installed, output: result.output.slice(-2e3) };
620
+ }
621
+ return { ok: true, installed, output: result.output.slice(-2e3) };
622
+ }
623
+ last = result;
624
+ if (result.spawnFailed) {
625
+ return { ok: false, errorKey: "update.errorFailed", output: result.output, summary: result.output };
626
+ }
627
+ notYetPublished = isNotYetPublished(result.output);
628
+ if (!notYetPublished) break;
629
+ if (attempt < attempts) await sleep(retryDelayMs);
630
+ }
631
+ }
632
+ return {
633
+ ok: false,
634
+ errorKey: notYetPublished ? "update.errorNotYetPublished" : "update.errorFailed",
635
+ output: String(last.output || "").slice(-2e3),
636
+ summary: npmErrorSummary(last.output)
637
+ };
638
+ }
555
639
  module.exports = {
556
640
  PACKAGE_NAME,
557
641
  DEFAULT_REGISTRY,
@@ -561,8 +645,11 @@ var require_updater = __commonJS({
561
645
  compareVersions,
562
646
  currentVersion,
563
647
  installKind: installKind2,
648
+ installedVersionOnDisk,
649
+ npmErrorSummary,
564
650
  performUpdate: performUpdate2,
565
- registryCandidates
651
+ registryCandidates,
652
+ updateChecksEnabled: updateChecksEnabled2
566
653
  };
567
654
  }
568
655
  });
@@ -910,16 +997,22 @@ var require_en = __commonJS({
910
997
  "hint.save": "s save",
911
998
  "hint.editing": "\u21B5 confirm \xB7 esc cancel",
912
999
  "update.title": "Updates",
1000
+ "update.banner": "herdr-remote {latest} is out (running {current}). Press 7 and Enter on the update row to install.",
913
1001
  "update.check": "Check for updates",
914
1002
  "update.checking": "Checking npm\u2026",
915
1003
  "update.upToDate": "Up to date ({version})",
916
1004
  "update.available": "Update {version} available (press Enter to install)",
917
1005
  "update.updating": "Installing {version}\u2026",
1006
+ "update.updatingRetry": "Installing {version}\u2026 (npm has not caught up yet, attempt {attempt})",
918
1007
  "update.done": "Updated to {version}",
919
1008
  "update.restartHint": "Update installed. Restart herdr-remote to apply.",
920
1009
  "update.errorNetwork": "Could not reach any npm registry",
921
1010
  "update.errorNetworkDetail": "No registry answered: {message}",
922
- "update.errorFailed": "Update failed. Run: npm install -g herdr-remote@latest",
1011
+ "update.errorFailed": "Update failed. Run: npm install -g herdr-remote@{version} --prefer-online",
1012
+ "update.errorFailedDetail": "npm: {message}",
1013
+ "update.errorNotYetPublished": "npm has not caught up with {version} yet (common in the first minutes after a release). Press Enter to try again shortly.",
1014
+ "update.errorNotApplied": "npm reported success, but {installed} is still installed",
1015
+ "update.mirrorBehind": "{registries} still has an older version; installing from {source}",
923
1016
  "update.cannot.source": "Source checkout \u2014 update with git",
924
1017
  "update.cannot.linked": "Linked package \u2014 managed by npm link"
925
1018
  };
@@ -1131,16 +1224,22 @@ var require_zh = __commonJS({
1131
1224
  "hint.save": "s \u4FDD\u5B58",
1132
1225
  "hint.editing": "\u21B5 \u786E\u8BA4 \xB7 esc \u53D6\u6D88",
1133
1226
  "update.title": "\u66F4\u65B0",
1227
+ "update.banner": "herdr-remote {latest} \u5DF2\u53D1\u5E03\uFF08\u5F53\u524D {current}\uFF09\uFF0C\u6309 7 \u8FDB\u5165\u300C\u8BED\u8A00\u4E0E\u5173\u4E8E\u300D\uFF0C\u5728\u66F4\u65B0\u4E00\u884C\u6309 Enter \u5B89\u88C5\u3002",
1134
1228
  "update.check": "\u68C0\u67E5\u66F4\u65B0",
1135
1229
  "update.checking": "\u67E5\u8BE2 npm \u4E2D\u2026",
1136
1230
  "update.upToDate": "\u5DF2\u662F\u6700\u65B0\uFF08{version}\uFF09",
1137
1231
  "update.available": "\u65B0\u7248\u672C {version} \u53EF\u7528\uFF08\u6309 Enter \u5B89\u88C5\uFF09",
1138
1232
  "update.updating": "\u5B89\u88C5 {version} \u4E2D\u2026",
1233
+ "update.updatingRetry": "\u5B89\u88C5 {version} \u4E2D\u2026\uFF08npm \u5C1A\u672A\u540C\u6B65\uFF0C\u7B2C {attempt} \u6B21\u5C1D\u8BD5\uFF09",
1139
1234
  "update.done": "\u5DF2\u66F4\u65B0\u81F3 {version}",
1140
1235
  "update.restartHint": "\u66F4\u65B0\u5DF2\u5B89\u88C5\uFF0C\u91CD\u542F herdr-remote \u751F\u6548\u3002",
1141
1236
  "update.errorNetwork": "\u6240\u6709 npm registry \u5747\u65E0\u6CD5\u8FDE\u63A5",
1142
1237
  "update.errorNetworkDetail": "\u65E0 registry \u5E94\u7B54\uFF1A{message}",
1143
- "update.errorFailed": "\u66F4\u65B0\u5931\u8D25\uFF0C\u53EF\u624B\u52A8\u6267\u884C\uFF1Anpm install -g herdr-remote@latest",
1238
+ "update.errorFailed": "\u66F4\u65B0\u5931\u8D25\uFF0C\u53EF\u624B\u52A8\u6267\u884C\uFF1Anpm install -g herdr-remote@{version} --prefer-online",
1239
+ "update.errorFailedDetail": "npm\uFF1A{message}",
1240
+ "update.errorNotYetPublished": "npm \u8FD8\u6CA1\u540C\u6B65\u5230 {version}\uFF08\u521A\u53D1\u5E03\u7684\u51E0\u5206\u949F\u5185\u5E38\u89C1\uFF09\uFF0C\u7A0D\u540E\u6309 Enter \u91CD\u8BD5\u3002",
1241
+ "update.errorNotApplied": "npm \u62A5\u544A\u6210\u529F\uFF0C\u4F46\u5B89\u88C5\u7684\u4ECD\u662F {installed}",
1242
+ "update.mirrorBehind": "{registries} \u4ECD\u662F\u65E7\u7248\u672C\uFF0C\u5C06\u4ECE {source} \u5B89\u88C5",
1144
1243
  "update.cannot.source": "\u6E90\u7801\u76EE\u5F55\u8FD0\u884C\uFF0C\u8BF7\u4F7F\u7528 git \u66F4\u65B0",
1145
1244
  "update.cannot.linked": "npm link \u5DE5\u4F5C\u526F\u672C\uFF0C\u7531 npm link \u7BA1\u7406"
1146
1245
  };
@@ -1728,6 +1827,1161 @@ var require_terminal_palette = __commonJS({
1728
1827
  }
1729
1828
  });
1730
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
+
1731
2985
  // src/socket-discovery.js
1732
2986
  var require_socket_discovery = __commonJS({
1733
2987
  "src/socket-discovery.js"(exports, module) {
@@ -1816,6 +3070,7 @@ var require_service = __commonJS({
1816
3070
  rememberTerminalPalette,
1817
3071
  rememberedTerminalPalette
1818
3072
  } = require_terminal_palette();
3073
+ var { resolveHostFont } = require_terminal_font();
1819
3074
  var { resolveSocketPath } = require_socket_discovery();
1820
3075
  var { preferredLanAddress: preferredLanAddress2 } = require_net_interfaces();
1821
3076
  var { MIN_HERDR_VERSION: MIN_HERDR_VERSION2, findHerdrCommand, herdrVersion: herdrVersion2, resolveHerdrCommand } = require_herdr_command();
@@ -1872,6 +3127,9 @@ var require_service = __commonJS({
1872
3127
  cachedTerminalPalette = rememberedTerminalPalette();
1873
3128
  return cachedTerminalPalette;
1874
3129
  }
3130
+ function hostTerminalFont() {
3131
+ return resolveHostFont();
3132
+ }
1875
3133
  function logPath(name) {
1876
3134
  return path.join(stateDir2(), `${name}.log`);
1877
3135
  }
@@ -1913,6 +3171,7 @@ var require_service = __commonJS({
1913
3171
  });
1914
3172
  }
1915
3173
  const terminalPalette = hostTerminalPalette();
3174
+ const terminalFont = hostTerminalFont();
1916
3175
  specs.push({
1917
3176
  name: "host",
1918
3177
  command: process.execPath,
@@ -1921,6 +3180,7 @@ var require_service = __commonJS({
1921
3180
  // Captured here, where a terminal may still be attached, because the
1922
3181
  // connector itself usually runs detached with no terminal to ask.
1923
3182
  ...terminalPalette ? { HERDR_TERM_PALETTE_JSON: JSON.stringify(terminalPalette) } : {},
3183
+ ...terminalFont ? { HERDR_TERM_FONT_JSON: JSON.stringify(terminalFont) } : {},
1924
3184
  RELAY_URL: resolveHostRelayUrl(config),
1925
3185
  RELAY_HOST_ID: state.hostId,
1926
3186
  RELAY_HOST_TOKEN: state.hostToken,
@@ -2198,6 +3458,7 @@ var require_service = __commonJS({
2198
3458
  serviceSpecs,
2199
3459
  baseEnvironment,
2200
3460
  hostTerminalPalette,
3461
+ hostTerminalFont,
2201
3462
  startServices,
2202
3463
  stopServices,
2203
3464
  restartServices,
@@ -2625,7 +3886,7 @@ var require_lifecycle = __commonJS({
2625
3886
  import { render } from "ink";
2626
3887
 
2627
3888
  // tui/src/App.tsx
2628
- import { useCallback as useCallback2, useEffect as useEffect6, useMemo as useMemo4, useRef as useRef3, useState as useState10 } from "react";
3889
+ import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef3, useState as useState10 } from "react";
2629
3890
  import { Box as Box11, Text as Text11, useApp, useInput as useInput9 } from "ink";
2630
3891
 
2631
3892
  // tui/src/mouse/index.tsx
@@ -3819,14 +5080,16 @@ function HerdrScreen({ ctx }) {
3819
5080
  }
3820
5081
 
3821
5082
  // tui/src/screens/About.tsx
3822
- import { useState as useState8 } from "react";
5083
+ import { useEffect as useEffect6, useState as useState8 } from "react";
3823
5084
  import { Box as Box9, Text as Text9, useInput as useInput7 } from "ink";
3824
5085
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
5086
+ var registryHost = (registry) => registry.replace(/^https?:\/\//, "");
3825
5087
  function About({ ctx }) {
3826
5088
  const { t, draft } = ctx;
3827
5089
  const [selected, setSelected] = useState8("auto");
3828
5090
  const [update, setUpdate] = useState8({ phase: "idle" });
3829
- const registryRef = useState8(() => ({ current: "" }))[0];
5091
+ const [sourceNote, setSourceNote] = useState8(null);
5092
+ const checkRef = useState8(() => ({ current: { registry: "", sources: [] } }))[0];
3830
5093
  const detected = detectLocale({ preference: "auto" });
3831
5094
  const languageOptions = [
3832
5095
  { id: "auto", label: t("about.languageAuto", { detected }) },
@@ -3843,36 +5106,57 @@ function About({ ctx }) {
3843
5106
  case "available":
3844
5107
  return t("update.available", { version: update.latest });
3845
5108
  case "updating":
3846
- return t("update.updating", { version: update.latest });
5109
+ return update.attempt > 1 ? t("update.updatingRetry", { version: update.latest, attempt: update.attempt }) : t("update.updating", { version: update.latest });
3847
5110
  case "done":
3848
5111
  return t("update.done", { version: update.latest });
3849
5112
  case "error":
3850
- return t(update.messageKey);
5113
+ return t(update.messageKey, update.params);
3851
5114
  default:
3852
5115
  return t("update.check");
3853
5116
  }
3854
5117
  })();
3855
5118
  const options = [...languageOptions, { id: "update", label: updateLabel }];
5119
+ const applyCheck = (result) => {
5120
+ checkRef.current = { registry: result.registry ?? "", sources: result.sources ?? [] };
5121
+ const behind = result.behind ?? [];
5122
+ setSourceNote(result.updateAvailable && behind.length > 0 && result.registry ? t("update.mirrorBehind", {
5123
+ registries: behind.map((entry) => `${registryHost(entry.registry)} ${entry.version ?? ""}`.trim()).join(", "),
5124
+ source: registryHost(result.registry)
5125
+ }) : null);
5126
+ setUpdate(result.updateAvailable ? { phase: "available", latest: result.latest } : { phase: "current", latest: result.latest });
5127
+ };
5128
+ useEffect6(() => {
5129
+ if (ctx.updateCheck?.ok && update.phase === "idle") applyCheck(ctx.updateCheck);
5130
+ }, [ctx.updateCheck]);
3856
5131
  const runCheck = async () => {
3857
5132
  setUpdate({ phase: "checking" });
5133
+ setSourceNote(null);
3858
5134
  const result = await (0, import_updater.checkForUpdate)();
3859
5135
  if (!result.ok) {
3860
5136
  setUpdate({ phase: "error", messageKey: result.errorKey ?? "update.errorNetwork" });
3861
5137
  if (result.message) ctx.notify(t("update.errorNetworkDetail", { message: result.message }), "error");
3862
5138
  return;
3863
5139
  }
3864
- registryRef.current = result.registry ?? "";
3865
- setUpdate(result.updateAvailable ? { phase: "available", latest: result.latest } : { phase: "current", latest: result.latest });
5140
+ ctx.setUpdateCheck(result);
5141
+ applyCheck(result);
3866
5142
  };
3867
5143
  const runUpdate = async (latest) => {
3868
- setUpdate({ phase: "updating", latest });
3869
- const result = await (0, import_updater.performUpdate)({ registry: registryRef.current });
5144
+ setUpdate({ phase: "updating", latest, attempt: 1 });
5145
+ const result = await (0, import_updater.performUpdate)({
5146
+ registry: checkRef.current.registry,
5147
+ sources: checkRef.current.sources,
5148
+ version: latest,
5149
+ onAttempt: ({ attempt }) => setUpdate({ phase: "updating", latest, attempt })
5150
+ });
3870
5151
  if (!result.ok) {
3871
- setUpdate({ phase: "error", messageKey: result.errorKey ?? "update.errorFailed" });
3872
- ctx.notify(t("update.errorFailed"), "error");
5152
+ const params = { version: latest, installed: result.installed ?? "" };
5153
+ setUpdate({ phase: "error", messageKey: result.errorKey ?? "update.errorFailed", params });
5154
+ ctx.notify(result.summary ? t("update.errorFailedDetail", { message: result.summary }) : t(result.errorKey ?? "update.errorFailed", params), "error");
3873
5155
  return;
3874
5156
  }
3875
5157
  setUpdate({ phase: "done", latest });
5158
+ setSourceNote(null);
5159
+ ctx.setUpdateCheck(null);
3876
5160
  ctx.notify(t("update.restartHint"), "success");
3877
5161
  };
3878
5162
  const activateUpdate = () => {
@@ -3931,19 +5215,22 @@ function About({ ctx }) {
3931
5215
  option.id
3932
5216
  )) }),
3933
5217
  /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("update.title") }),
3934
- /* @__PURE__ */ jsx10(Box9, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx10(
3935
- Selectable,
3936
- {
3937
- selected: selected === "update",
3938
- onSelect: () => {
3939
- setSelected("update");
3940
- activateUpdate();
3941
- },
3942
- onHover: () => setSelected("update"),
3943
- children: updateLabel
3944
- }
3945
- ) }),
3946
- /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.16" }) }),
5218
+ /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [
5219
+ /* @__PURE__ */ jsx10(
5220
+ Selectable,
5221
+ {
5222
+ selected: selected === "update",
5223
+ onSelect: () => {
5224
+ setSelected("update");
5225
+ activateUpdate();
5226
+ },
5227
+ onHover: () => setSelected("update"),
5228
+ children: updateLabel
5229
+ }
5230
+ ),
5231
+ sourceNote ? /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: ` ${sourceNote}` }) : null
5232
+ ] }),
5233
+ /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.18" }) }),
3947
5234
  /* @__PURE__ */ jsx10(Row, { label: t("about.configPath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.configPath)() }) }),
3948
5235
  /* @__PURE__ */ jsx10(Row, { label: t("about.statePath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.stateDir)() }) }),
3949
5236
  /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.docs") }) }),
@@ -4260,7 +5547,11 @@ function Tab({ label, active, onSelect }) {
4260
5547
  function Footer({ hints }) {
4261
5548
  return /* @__PURE__ */ jsx12(Box11, { marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx12(Text11, { color: theme.muted, children: hints.join(" \xB7 ") }) });
4262
5549
  }
4263
- function App({ initialLanguage, needsWizard }) {
5550
+ function App({
5551
+ initialLanguage,
5552
+ needsWizard,
5553
+ updateChecker = (0, import_updater.updateChecksEnabled)() ? import_updater.checkForUpdate : null
5554
+ }) {
4264
5555
  const { exit } = useApp();
4265
5556
  const mouse = useMouse();
4266
5557
  const [config, setConfig] = useState10(() => (0, import_config.loadConfig)());
@@ -4272,6 +5563,18 @@ function App({ initialLanguage, needsWizard }) {
4272
5563
  const [busy, setBusy] = useState10(false);
4273
5564
  const [editingId, setEditingId] = useState10(null);
4274
5565
  const [wizardDone, setWizardDone] = useState10(!needsWizard);
5566
+ const [updateCheck, setUpdateCheck] = useState10(null);
5567
+ useEffect7(() => {
5568
+ if (!updateChecker) return void 0;
5569
+ let cancelled = false;
5570
+ updateChecker().then((result) => {
5571
+ if (!cancelled && result?.ok) setUpdateCheck(result);
5572
+ }).catch(() => {
5573
+ });
5574
+ return () => {
5575
+ cancelled = true;
5576
+ };
5577
+ }, [updateChecker]);
4275
5578
  const locale = useMemo4(
4276
5579
  () => detectLocale({ preference: initialLanguage ?? draft.ui.language }),
4277
5580
  [initialLanguage, draft.ui.language]
@@ -4298,7 +5601,7 @@ function App({ initialLanguage, needsWizard }) {
4298
5601
  refresh();
4299
5602
  });
4300
5603
  }, [notify, refresh]);
4301
- useEffect6(() => {
5604
+ useEffect7(() => {
4302
5605
  if (!wizardDone) return void 0;
4303
5606
  refresh();
4304
5607
  const timer = setInterval(() => {
@@ -4307,7 +5610,7 @@ function App({ initialLanguage, needsWizard }) {
4307
5610
  return () => clearInterval(timer);
4308
5611
  }, [wizardDone, refresh, editingId]);
4309
5612
  const mouseChoice = useRef3(null);
4310
- useEffect6(() => {
5613
+ useEffect7(() => {
4311
5614
  if (mouseChoice.current !== null) return;
4312
5615
  if (mouse.supported && !mouse.enabled) mouse.enable();
4313
5616
  }, [mouse.supported, mouse.enabled, mouse]);
@@ -4327,7 +5630,9 @@ function App({ initialLanguage, needsWizard }) {
4327
5630
  notify,
4328
5631
  run,
4329
5632
  refresh,
4330
- message
5633
+ message,
5634
+ updateCheck,
5635
+ setUpdateCheck
4331
5636
  };
4332
5637
  useInput9((input, key) => {
4333
5638
  if (key.ctrl && input === "c") {
@@ -4387,9 +5692,12 @@ function App({ initialLanguage, needsWizard }) {
4387
5692
  t("hint.quit")
4388
5693
  ].filter(Boolean);
4389
5694
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [
4390
- /* @__PURE__ */ jsxs11(Box11, { marginBottom: 1, children: [
4391
- /* @__PURE__ */ jsx12(Text11, { bold: true, children: t("app.name") }),
4392
- /* @__PURE__ */ jsx12(Text11, { color: theme.muted, children: ` ${t("app.tagline")}` })
5695
+ /* @__PURE__ */ jsxs11(Box11, { marginBottom: 1, flexDirection: "column", children: [
5696
+ /* @__PURE__ */ jsxs11(Box11, { children: [
5697
+ /* @__PURE__ */ jsx12(Text11, { bold: true, children: t("app.name") }),
5698
+ /* @__PURE__ */ jsx12(Text11, { color: theme.muted, children: ` ${t("app.tagline")}` })
5699
+ ] }),
5700
+ updateCheck?.updateAvailable ? /* @__PURE__ */ jsx12(Text11, { color: theme.warn, children: t("update.banner", { latest: updateCheck.latest ?? "", current: updateCheck.current }) }) : null
4393
5701
  ] }),
4394
5702
  /* @__PURE__ */ jsx12(Box11, { marginBottom: 1, children: TABS.map((entry, entryIndex) => /* @__PURE__ */ jsx12(
4395
5703
  Tab,