dsh-folia 1.6.0 → 2.0.0
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/README.beginner.md +13 -13
- package/README.en.md +25 -25
- package/README.md +378 -62
- package/assets/about/README.md +89 -0
- package/assets/readme/mascot-eating-rice-1280x1784.jpg +0 -0
- package/cordis.patch.yml +16 -86
- package/lib/about/README.md +2 -2
- package/lib/client.js +3736 -89
- package/lib/components/about-notice/index.js +125 -0
- package/lib/components/about-notice/routes/about-qr.js +92 -0
- package/lib/components/about-notice/routes/github-stars.js +143 -0
- package/lib/components/fonts/client.build.json +22 -0
- package/lib/components/fonts/index.js +129 -0
- package/lib/components/fonts/lib/client.fonts.js +656 -0
- package/lib/components/fonts/routes/fontsets.js +415 -0
- package/lib/components/fonts/routes/system-fonts.js +530 -0
- package/lib/components/fonts/src/appearance-font-section.js +389 -0
- package/lib/components/fonts/src/client.js +82 -0
- package/lib/components/fonts/src/fontset-editor.js +209 -0
- package/lib/components/fx/client.build.json +16 -0
- package/lib/components/fx/index.js +44 -0
- package/lib/components/fx/lib/client.fx.js +847 -0
- package/lib/components/fx/src/client.js +75 -0
- package/lib/components/fx/src/ext-fx.js +155 -0
- package/lib/components/fx/src/fx-layer.js +635 -0
- package/lib/components/mascot-avatar/index.js +147 -0
- package/lib/components/mascot-avatar/routes/avatar.js +137 -0
- package/lib/components/mascot-avatar/routes/mascot.js +124 -0
- package/lib/components/media-bridge/audio-tap.swift +146 -0
- package/lib/components/media-bridge/backend.js +144 -0
- package/lib/components/media-bridge/index.js +129 -0
- package/lib/components/media-bridge/legacy.js +467 -0
- package/lib/components/media-bridge/provision.js +301 -0
- package/lib/components/media-bridge/routes/now-playing.js +177 -0
- package/lib/components/media-bridge/supervisor.js +765 -0
- package/lib/components/parallax/client.build.json +16 -0
- package/lib/components/parallax/index.js +44 -0
- package/lib/components/parallax/lib/client.parallax.js +1492 -0
- package/lib/components/parallax/src/client.js +60 -0
- package/lib/components/parallax/src/ext-parallax.js +148 -0
- package/lib/components/parallax/src/parallax-layer.js +1299 -0
- package/lib/components/pet-link/client.build.json +21 -0
- package/lib/components/pet-link/index.js +34 -0
- package/lib/components/pet-link/lib/client.pet-link.js +641 -0
- package/lib/components/pet-link/official-provider.js +151 -0
- package/lib/components/pet-link/prepared.js +10 -0
- package/lib/components/pet-link/routes/official-pet.js +118 -0
- package/lib/components/pet-link/src/client.js +55 -0
- package/lib/components/pet-link/src/official-pet.js +130 -0
- package/lib/components/pet-link/src/pet-link-actions.json +18 -0
- package/lib/components/pet-link/src/pet-link-timings.json +61 -0
- package/lib/components/pet-link/src/pet-link-tuner.js +142 -0
- package/lib/components/pet-link/src/pet-link.js +295 -0
- package/lib/diag-trace.js +1 -1
- package/lib/faststart.js +3 -3
- package/lib/host-boot.js +3 -3
- package/lib/index.js +51 -13
- package/lib/inventory.js +2 -2
- package/lib/kernel/component-session.js +3 -3
- package/lib/kernel/config.js +4 -4
- package/lib/kernel/data-dir.js +55 -9
- package/lib/kernel/host-session.js +4 -4
- package/lib/log.js +3 -3
- package/lib/media/frame-store.js +2 -2
- package/lib/media/media-dirs.js +1 -1
- package/lib/media/transcode.js +10 -10
- package/lib/notice.js +5 -5
- package/lib/routes/cache-dir.js +2 -2
- package/lib/routes/inventory.js +93 -33
- package/lib/routes/live-frame.js +1 -1
- package/lib/routes/media-bytes.js +1 -1
- package/lib/routes/occlusion.js +2 -2
- package/lib/routes/props.js +1 -1
- package/lib/routes/settings.js +3 -3
- package/lib/routes/we-assets.js +1 -1
- package/lib/serve.js +1 -1
- package/lib/settings-schema.js +5 -5
- package/lib/startup-sweep.js +1 -1
- package/lib/steam-discovery.js +5 -5
- package/lib/subpackage-boot.js +11 -11
- package/lib/types/client.d.ts +2 -2
- package/lib/types/index.d.ts +15 -15
- package/lib/upload-dir.js +3 -3
- package/lib/we-assets.js +4 -4
- package/lib/webwallgl/renderer.js +1 -1
- package/package.json +149 -22
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes/system-fonts.js — **本机字体清单**的唯一一条路由:`GET <BASE>/system-fonts`
|
|
3
|
+
* (设置页「全局字体 / 终端字体」两个下拉里那一组「本机字体」的数据源)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么独立成文件:这一族要回答的问题与别处都不同 —— "字体清单从哪来(三平台各自的工具)、
|
|
6
|
+
* 工具不在时给什么、一次扫描很贵所以缓存放哪、缓存放多久"。写进 `lib/index.js` 只会让那个门面
|
|
7
|
+
* 多一段与壁纸无关的进程调用。
|
|
8
|
+
*
|
|
9
|
+
* ══ 清单从哪来(**权威**来源可有多条,取并集;失败才降级)══════════════════════════════
|
|
10
|
+
* · macOS **两条腿**(并行跑,取名字并集 —— 这是"两种写法都能选到"的原因):
|
|
11
|
+
* ① `system_profiler SPFontsDataType -json -detailLevel mini` → `typefaces[].family`
|
|
12
|
+
* = **本地化**族名(中文系统上 `PingFang SC` 报成 `苹方-简`)。**实测**:本机 367 个
|
|
13
|
+
* 字体文件 → 359 个族名(含 `.` 开头的私有族);耗时 ~10s ⇒ 必须缓存,见下。
|
|
14
|
+
* ② CoreText(`osascript -l JavaScript` → `CTFontManagerCopyAvailableFontFamilyNames`)
|
|
15
|
+
* = **规范(英文)族名**:`PingFang SC` / `Heiti SC` / `Songti SC` … 实测 ~0.05s。
|
|
16
|
+
* 为什么必须有它:CSS / 字体册英文界面 / 设计软件用的都是这套名字,本插件的内置族键
|
|
17
|
+
* (`STXingkai` / `KaiTi` …)也是英文名 —— 只有本地化名时用户找不到自己认识的字体,
|
|
18
|
+
* 看着就像"没扫全"(**用户报的缺陷**)。
|
|
19
|
+
* · Windows `powershell … InstalledFontCollection`(族名是 .NET 那边的权威口径);
|
|
20
|
+
* 拿不到时退回注册表 `Fonts` 键的**值名**(那是"字体名"不是族名 ⇒ 标 approximate)
|
|
21
|
+
* · Linux `fc-list --format=%{family}\n`(fontconfig 的族名口径,一行可含多个别名逗号分隔)
|
|
22
|
+
* 权威来源一条都没拿到(工具不在 / 调用失败 / 超时)时走 `parseFontFileNames`:扫字体目录按
|
|
23
|
+
* **文件名**推族名,并**如实标 `approximate: true`** —— 面板据此显示"按文件名推测"。宁可说清是
|
|
24
|
+
* 推测,也不要让一个猜出来的名字看着像真的(与「内嵌 MP4 探测未命中一律 未知」同一口径)。
|
|
25
|
+
*
|
|
26
|
+
* ══ 缓存(一次扫描 10s,绝不能每次开面板都付)══════════════════════════════════════
|
|
27
|
+
* 内存 + 落盘(`cachePath()`,`pluginDataDir()/system-fonts.json`)双层;TTL 见
|
|
28
|
+
* `SYSTEM_FONTS_TTL_MS`。过期**照旧先回旧值**(标 `stale: true`)、后台重扫 —— 面板永远
|
|
29
|
+
* 是"先有东西可选",不会因为一次扫描把界面卡住。`?refresh=1` 强制重扫(面板上的「重新扫描」)。
|
|
30
|
+
* 并发请求合并成一次扫描(与 `star-count` 同形)。
|
|
31
|
+
*
|
|
32
|
+
* 不变量:
|
|
33
|
+
* · **不猜**:权威来源给的族名原样使用(只去重排序 + 过滤私有族);只有降级路径的结果才带
|
|
34
|
+
* `approximate` 标记。
|
|
35
|
+
* · **绝不把失败甩成 5xx**:任何失败都回 200 + `ok:false` + 原因(清单给空数组,面板说得出话)。
|
|
36
|
+
* · **有界**:单次进程输出有上限、清单有条数上限、没有超时的子进程不留在后台
|
|
37
|
+
* (超时即 kill,之后走降级路径)—— 一个挂住的 `system_profiler` 不许拖住整个插件。
|
|
38
|
+
* · **子进程输出必须按字节收、最后一次性解码**:逐块 `toString('utf8')` 会在多字节字符跨块时
|
|
39
|
+
* 插进 U+FFFD(实测 `系统字体` → `系统\uFFFD\uFFFD\uFFFD体`)⇒ 清单里出现认不出的族名。
|
|
40
|
+
* · **过滤掉不可用的名字**:`.` 开头的私有族、以及过不了共享内核 `systemFontKeyOf`
|
|
41
|
+
* 校验的名字(含控制符 / 引号 / 括号类破坏 CSS 的字符)一律不进清单 ⇒ 前端拿到的
|
|
42
|
+
* 每个名字都能安全变成 `sys:` 族键。
|
|
43
|
+
* · **扫描不在启动期发生**:只有第一次请求这条路才触发(`apply()` 一个字节都不写,
|
|
44
|
+
* 与字体集迁移同一条纪律)。
|
|
45
|
+
*
|
|
46
|
+
* 契约:`registerSystemFontsRoutes(webServer, c)`;`c` 里是这一族用到但不属于它的东西:
|
|
47
|
+
* · `disposers` / `base: BASE` ← 清理句柄与路径前缀
|
|
48
|
+
* · `cachePath()` ← 落盘缓存路径(`pluginDataDir()/system-fonts.json`)
|
|
49
|
+
* · `log` ← 降级要留痕(成功不记:成功是常态)
|
|
50
|
+
* · `schema` ← 共享内核的纯符号面(本族只用 `systemFontKeyOf`,见下)
|
|
51
|
+
* · `http` ← JSON 应答的纯函数面(本族只用 `sendJson`,见下)
|
|
52
|
+
* · `platform` / `home` / `fontDirs` / `runFontCommand` ← **可选**注入点:真实调用一个都不传
|
|
53
|
+
* (各自有默认实现),守卫用它们换掉平台、目录与子进程(本族**不 spawn** 真进程就能判)。
|
|
54
|
+
* 本文件自己的纯函数(`collectSystemFonts` / `defaultRun` / 各 `parse*`)**导出**:守卫用夹具判前者,
|
|
55
|
+
* 用**真子进程**判 `defaultRun` 的解码(那一条没有夹具能替)。
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import { spawn } from 'node:child_process';
|
|
59
|
+
import { existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
60
|
+
import { homedir } from 'node:os';
|
|
61
|
+
import { join } from 'node:path';
|
|
62
|
+
// 跨包通道(ADR-0014 S5.1 / S5.3):本族**零主包 specifier** —— 共享内核的纯符号面也不静态
|
|
63
|
+
// import,而由 `registerSystemFontsRoutes` 的 `c` 显式收下(调用点 = 子包入口
|
|
64
|
+
// packages/fonts/lib/index.js,值来自 `ctx.wallpaperEngineHost`):
|
|
65
|
+
// · `c.schema.systemFontKeyOf` —— 名字的值域与前端**同一份**(这里过滤掉的名字,前端也一定拼不出族键)。
|
|
66
|
+
// · `c.http.sendJson` —— JSON 应答的**唯一实现**(状态码 + 两个头 + end,见主包 lib/json-response.js)。
|
|
67
|
+
// 运行期才存在的实例(disposers / cachePath / log / 平台注入点)仍逐键从 `c` 消费。
|
|
68
|
+
// 模块作用域**不持有**主包的任何值(ADR-0014 S5.1 / S5.3 反模式:HMR 后旧代 exports)。
|
|
69
|
+
|
|
70
|
+
/** 权威扫描的成功结果新鲜期:字体装/删不频繁,一次扫描(macOS ~10s)摊到一周可接受。 */
|
|
71
|
+
const SYSTEM_FONTS_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
72
|
+
/**
|
|
73
|
+
* **落盘缓存的口径版本**:`readDisk` 只认这个版本,别的(含没有这个字段的老缓存)一律当"没有缓存"
|
|
74
|
+
* ⇒ 立刻重扫。
|
|
75
|
+
*
|
|
76
|
+
* 为什么必须有它:TTL 是一周,而"清单长什么样"会随**代码**变(加了一条腿、改了过滤规则、修了解码)。
|
|
77
|
+
* 没有版本号时,用户升级插件后最长一周里拿到的还是**老口径**的结果 —— 表现就是"改了没生效",
|
|
78
|
+
* 而且看不出是缓存。**改动枚举口径(来源、过滤、解码)时必须 +1。**
|
|
79
|
+
*/
|
|
80
|
+
const SYSTEM_FONTS_CACHE_VERSION = 2;
|
|
81
|
+
/** 单个子进程的超时:macOS 实测 ~10s,25s 给慢机器留余量;超时即 kill 并降级。 */
|
|
82
|
+
const FONT_SCAN_TIMEOUT_MS = 25000;
|
|
83
|
+
/** 子进程输出上限(macOS 的 JSON 实测 ~2MB):够用,又不让一次异常输出吃光内存。 */
|
|
84
|
+
const FONT_SCAN_MAX_BYTES = 16 * 1024 * 1024;
|
|
85
|
+
/** 清单条数上限:一台机器几百到几千个族名,超出即截断(只影响这份"可选清单")。 */
|
|
86
|
+
const SYSTEM_FONTS_MAX = 2000;
|
|
87
|
+
/** 目录扫描(降级路径)的递归深度与文件数上限:不让一次扫目录变成无界遍历。 */
|
|
88
|
+
const FONT_DIR_MAX_DEPTH = 3;
|
|
89
|
+
const FONT_DIR_MAX_FILES = 4000;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* macOS 的第二条腿:**CoreText 的规范族名**(英文名)—— 一条 JXA 脚本,实测几十毫秒。
|
|
93
|
+
*
|
|
94
|
+
* 为什么需要它(**用户报的"没扫出本机所有字体"的直接修法**):`system_profiler` 报的是
|
|
95
|
+
* **本地化**族名 —— 中文系统上 `PingFang SC` 报成 `苹方-简`、`Heiti SC` 报成 `黑体-简`。
|
|
96
|
+
* 而那套英文名才是 CSS / 字体册(英文界面)/ 设计软件里用的名字,本插件的**内置族键**
|
|
97
|
+
* (`STXingkai` / `KaiTi` / `SimHei` …)也是英文名 ⇒ 用户在"本机字体"里找不到自己认识的字体,
|
|
98
|
+
* 看着就像"没扫全"。两种名字在 Chromium 里命中同一个字体(实测 `"PingFang SC"` 与 `"苹方-简"`
|
|
99
|
+
* 的渲染宽度逐位相同)⇒ 两条腿的名字**取并集**,两种写法都能选到。
|
|
100
|
+
*
|
|
101
|
+
* 为什么走 `osascript -l JavaScript` 而不是 `fc-list`:macOS 默认没有 fontconfig;而 CoreText
|
|
102
|
+
* 自己的族名列表就是"系统认为存在哪些字体族"的权威答案(实测与本机字体册逐条对得上)。
|
|
103
|
+
* 它**只做枚举**(`CTFontManagerCopyAvailableFontFamilyNames`),不取本地化名 —— 那条路要穿过
|
|
104
|
+
* CFStringRef 的桥接(实测拿回来是 `[object Ref]`,不可靠),本地化名由 `system_profiler` 那条腿负责。
|
|
105
|
+
*/
|
|
106
|
+
const CORETEXT_LIST_ARGS = [
|
|
107
|
+
'-l', 'JavaScript', '-e',
|
|
108
|
+
'ObjC.import("CoreText");'
|
|
109
|
+
+ 'const arr=$.CTFontManagerCopyAvailableFontFamilyNames();'
|
|
110
|
+
+ 'const n=$.CFArrayGetCount(arr);'
|
|
111
|
+
+ 'const out=[];'
|
|
112
|
+
+ 'for(let i=0;i<n;i++)out.push(ObjC.unwrap(ObjC.castRefToObject($.CFArrayGetValueAtIndex(arr,i))));'
|
|
113
|
+
+ 'out.join("\\n");',
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
/** 按平台给**权威来源**(绝对路径优先 —— Electron 的 PATH 不保证含 /usr/sbin)。可有多条。 */
|
|
117
|
+
function authoritativeSources(platform) {
|
|
118
|
+
if (platform === 'darwin') {
|
|
119
|
+
const bin = existsSync('/usr/sbin/system_profiler') ? '/usr/sbin/system_profiler' : 'system_profiler';
|
|
120
|
+
// 两条腿**并行**跑:慢的那条决定总耗时,快的那条几乎免费 ⇒ 冷扫描不因为多一条而变慢。
|
|
121
|
+
return [
|
|
122
|
+
{ source: 'system_profiler', cmd: bin, args: ['SPFontsDataType', '-json', '-detailLevel', 'mini'], parse: parseMacFonts },
|
|
123
|
+
{ source: 'coretext', cmd: 'osascript', args: CORETEXT_LIST_ARGS, parse: parseLineFamilies },
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
if (platform === 'win32') {
|
|
127
|
+
return [{
|
|
128
|
+
source: 'powershell-fonts',
|
|
129
|
+
cmd: 'powershell.exe',
|
|
130
|
+
args: ['-NoProfile', '-NonInteractive', '-Command',
|
|
131
|
+
'Add-Type -AssemblyName System.Drawing; '
|
|
132
|
+
+ '(New-Object System.Drawing.Text.InstalledFontCollection).Families | ForEach-Object { $_.Name }'],
|
|
133
|
+
parse: parseLineFamilies,
|
|
134
|
+
}];
|
|
135
|
+
}
|
|
136
|
+
if (platform === 'linux' || platform === 'freebsd' || platform === 'openbsd') {
|
|
137
|
+
return [{ source: 'fc-list', cmd: 'fc-list', args: ['--format=%{family}\\n'], parse: parseFcList }];
|
|
138
|
+
}
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* **非**权威来源(只有 Windows 有):注册表的值名是"字体名"(可能带 Bold/Italic)而不是族名
|
|
144
|
+
* ⇒ 结果标 `approximate`。只在**权威**那条腿失败后才试。
|
|
145
|
+
*/
|
|
146
|
+
function fallbackSources(platform) {
|
|
147
|
+
if (platform === 'win32') {
|
|
148
|
+
return [{
|
|
149
|
+
source: 'registry',
|
|
150
|
+
cmd: 'reg.exe',
|
|
151
|
+
args: ['query', 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts'],
|
|
152
|
+
parse: parseWindowsFontRegistry,
|
|
153
|
+
}];
|
|
154
|
+
}
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── 纯解析(每个都只吃文本、只吐族名数组;守卫用夹具判它们)────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* macOS `system_profiler … -json`:每个条目是一个**字体文件**,族名在
|
|
162
|
+
* `typefaces[].family` 里(**不是**条目自己的 `_name` —— 那是文件名)。
|
|
163
|
+
* 形状不对一律给空数组(宁可走降级,也不猜)。
|
|
164
|
+
*/
|
|
165
|
+
function parseMacFonts(text) {
|
|
166
|
+
let doc;
|
|
167
|
+
try { doc = JSON.parse(String(text || '')); } catch { return []; }
|
|
168
|
+
const list = doc && Array.isArray(doc.SPFontsDataType) ? doc.SPFontsDataType : null;
|
|
169
|
+
if (!list) return [];
|
|
170
|
+
const out = [];
|
|
171
|
+
for (const item of list) {
|
|
172
|
+
const faces = item && Array.isArray(item.typefaces) ? item.typefaces : [];
|
|
173
|
+
for (const face of faces) {
|
|
174
|
+
if (face && typeof face.family === 'string') out.push(face.family);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** `fc-list --format=%{family}\n`:一行一个字体,族名可用逗号给多个别名(本地化名)。 */
|
|
181
|
+
function parseFcList(text) {
|
|
182
|
+
const out = [];
|
|
183
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
184
|
+
for (const name of line.split(',')) out.push(name);
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 一行一个族名(PowerShell 的 `$_.Name` 输出就是这个形状)。 */
|
|
190
|
+
function parseLineFamilies(text) {
|
|
191
|
+
return String(text || '').split(/\r?\n/);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Windows 注册表 `Fonts` 键的 `reg query` 输出。行形如
|
|
196
|
+
* ` Arial (TrueType) REG_SZ arial.ttf`
|
|
197
|
+
* 取**值名**并去掉结尾的 `(TrueType)` / `(OpenType)` 这类后缀。注意值名是"字体名"
|
|
198
|
+
* (可能带 Bold/Italic)而不是族名 ⇒ 调用方会把这条路径的结果标成 approximate。
|
|
199
|
+
*/
|
|
200
|
+
function parseWindowsFontRegistry(text) {
|
|
201
|
+
const out = [];
|
|
202
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
203
|
+
const line = raw.trim();
|
|
204
|
+
if (!line || /^HKEY/i.test(line)) continue;
|
|
205
|
+
const m = line.match(/^(.*?)\s+REG_\w+\s+/);
|
|
206
|
+
if (!m) continue;
|
|
207
|
+
out.push(m[1].replace(/\s*\((?:TrueType|OpenType|Type 1|All res)\)\s*$/i, ''));
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 文件名里常见的字重/字形词(降级路径把它们从文件名尾部摘掉换算族名)。 */
|
|
213
|
+
const FONT_STYLE_WORDS = [
|
|
214
|
+
'thin', 'extralight', 'ultralight', 'light', 'book', 'regular', 'normal', 'medium',
|
|
215
|
+
'semibold', 'demibold', 'demib', 'bold', 'extrabold', 'ultrabold', 'black', 'heavy',
|
|
216
|
+
'italic', 'oblique', 'condensed', 'narrow', 'expanded', 'wide', 'retina',
|
|
217
|
+
];
|
|
218
|
+
const FONT_FILE_RE = /\.(ttf|otf|ttc|dfont)$/i;
|
|
219
|
+
|
|
220
|
+
/** 文件名 → 族名(降级用:只做"去扩展名 + 摘掉尾部字重词"两件确定的事)。 */
|
|
221
|
+
function familyFromFileName(fileName) {
|
|
222
|
+
let base = String(fileName || '').replace(FONT_FILE_RE, '');
|
|
223
|
+
// `PingFang-SC-Regular` / `Arial_Bold_Italic` / `SourceSansPro Bold` 三种分隔都遇到。
|
|
224
|
+
const parts = base.split(/[-_,]/);
|
|
225
|
+
while (parts.length > 1 && FONT_STYLE_WORDS.includes(parts[parts.length - 1].toLowerCase())) parts.pop();
|
|
226
|
+
base = parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
227
|
+
return base;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 目录扫描(降级路径):递归收集字体文件名 → 推族名。 */
|
|
231
|
+
function parseFontFileNames(names) {
|
|
232
|
+
const out = [];
|
|
233
|
+
for (const n of Array.isArray(names) ? names : []) {
|
|
234
|
+
const family = familyFromFileName(n);
|
|
235
|
+
if (family) out.push(family);
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** 按平台列出字体目录(降级路径用;不存在的一律跳过)。 */
|
|
241
|
+
function fontDirs(platform, home) {
|
|
242
|
+
const dirs = [];
|
|
243
|
+
if (platform === 'darwin') {
|
|
244
|
+
dirs.push('/System/Library/Fonts', '/System/Library/Fonts/Supplemental', '/Library/Fonts',
|
|
245
|
+
join(home, 'Library', 'Fonts'));
|
|
246
|
+
} else if (platform === 'win32') {
|
|
247
|
+
const root = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
|
|
248
|
+
dirs.push(join(root, 'Fonts'));
|
|
249
|
+
const local = process.env.LOCALAPPDATA;
|
|
250
|
+
if (local) dirs.push(join(local, 'Microsoft', 'Windows', 'Fonts'));
|
|
251
|
+
} else {
|
|
252
|
+
dirs.push('/usr/share/fonts', '/usr/local/share/fonts', join(home, '.fonts'),
|
|
253
|
+
join(home, '.local', 'share', 'fonts'));
|
|
254
|
+
}
|
|
255
|
+
return dirs;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 递归收集字体文件名(有界:深度与文件数都封顶)。 */
|
|
259
|
+
function collectFontFiles(dirs, depth, files) {
|
|
260
|
+
if (depth > FONT_DIR_MAX_DEPTH || files.length >= FONT_DIR_MAX_FILES) return files;
|
|
261
|
+
for (const dir of dirs) {
|
|
262
|
+
let entries;
|
|
263
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
264
|
+
for (const e of entries) {
|
|
265
|
+
if (files.length >= FONT_DIR_MAX_FILES) return files;
|
|
266
|
+
const abs = join(dir, e.name);
|
|
267
|
+
if (typeof e.isDirectory === 'function' && e.isDirectory()) {
|
|
268
|
+
collectFontFiles([abs], depth + 1, files);
|
|
269
|
+
} else if (typeof e.isFile === 'function' && e.isFile() && FONT_FILE_RE.test(e.name)) {
|
|
270
|
+
files.push(e.name);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return files;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── 收口:名字 → 干净清单 ────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 名字数组 → 清单。三件事:去空白、丢**不可用**的名字(`.` 开头的私有族 / 过不了共享内核
|
|
281
|
+
* 校验的)、去重排序(`localeCompare`:中文名与拉丁名混排时按本地口径,比码点序可读)。
|
|
282
|
+
*
|
|
283
|
+
* `keyOf` 就是共享内核的 `systemFontKeyOf`(纯函数)—— 本文件**不再静态 import** 它:调用方
|
|
284
|
+
* (`registerSystemFontsRoutes` 经 `c.schema`,或守卫直接调用本导出)把面传进来。
|
|
285
|
+
* 本文件零主包 specifier ⇒ **没有模块级默认可退**:缺 face 时**显式抛错**
|
|
286
|
+
* (宁可响亮失败,也别静默按"全不过滤"跑出一份脏清单)。
|
|
287
|
+
*/
|
|
288
|
+
function normalizeFamilies(names, keyOf) {
|
|
289
|
+
if (typeof keyOf !== 'function') {
|
|
290
|
+
throw new Error('normalizeFamilies(names, keyOf):缺少 systemFontKeyOf 面(c.schema.systemFontKeyOf)');
|
|
291
|
+
}
|
|
292
|
+
const seen = new Set();
|
|
293
|
+
const out = [];
|
|
294
|
+
for (const raw of Array.isArray(names) ? names : []) {
|
|
295
|
+
const name = typeof raw === 'string' ? raw.trim() : '';
|
|
296
|
+
if (!name || name.startsWith('.')) continue; // `.Apple Color Emoji UI` 这类私有族
|
|
297
|
+
if (!keyOf(name)) continue; // 进不了 `sys:` 键的名字一律不要
|
|
298
|
+
const dedupe = name.toLowerCase();
|
|
299
|
+
if (seen.has(dedupe)) continue;
|
|
300
|
+
seen.add(dedupe);
|
|
301
|
+
out.push(name);
|
|
302
|
+
if (out.length >= SYSTEM_FONTS_MAX) break;
|
|
303
|
+
}
|
|
304
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** 默认的子进程执行器:有超时、有输出上限、超时即 kill。 */
|
|
309
|
+
function defaultRun(cmd, args) {
|
|
310
|
+
// ⚠️ Promise 执行器的形参**不能**叫 `resolve`:本仓有一条守卫判"路由模块不得引用 `lib/index.js`
|
|
311
|
+
// 单独 import 的名字"(那里 import 了 node:path 的 `resolve`),按名扫会把它当成越界。
|
|
312
|
+
return new Promise((done) => {
|
|
313
|
+
let child = null;
|
|
314
|
+
try {
|
|
315
|
+
child = spawn(cmd, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
316
|
+
} catch (err) {
|
|
317
|
+
done({ ok: false, stdout: '', error: String((err && err.message) || err) });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// ⚠️ **按字节收、最后一次性解码**:逐块 `chunk.toString('utf8')` 再拼,会在一个多字节字符
|
|
321
|
+
// 跨块时插进 U+FFFD —— 实测 `系统字体` 变成 `系统\uFFFD\uFFFD\uFFFD体`,而同一份输出里
|
|
322
|
+
// 那个正确的名字也在 ⇒ 清单里同时出现"好的"和"坏的"两条(用户看到的就是一个认不出的
|
|
323
|
+
// 字体族)。超上限即判失败:截断的 JSON 解析不出东西,宁可走降级也不给半份名单。
|
|
324
|
+
const chunks = [];
|
|
325
|
+
let bytes = 0;
|
|
326
|
+
let settled = false;
|
|
327
|
+
let overflow = false;
|
|
328
|
+
const finish = (result) => {
|
|
329
|
+
if (settled) return;
|
|
330
|
+
settled = true;
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
done(result);
|
|
333
|
+
};
|
|
334
|
+
const timer = setTimeout(() => {
|
|
335
|
+
// 判 pid 再 kill:`cmd` 不存在时 spawn 失败 ⇒ `pid === undefined`,此时 `kill()`
|
|
336
|
+
// 会同步挂死事件循环(不抛,`try/catch` 挡不住)。pid 未落地就没有进程可杀。
|
|
337
|
+
if (child.pid !== undefined) { try { child.kill('SIGKILL'); } catch { /* 已经退出 */ } }
|
|
338
|
+
finish({ ok: false, stdout: '', error: 'timeout' });
|
|
339
|
+
}, FONT_SCAN_TIMEOUT_MS);
|
|
340
|
+
if (timer && typeof timer.unref === 'function') timer.unref();
|
|
341
|
+
child.stdout.on('data', (buf) => {
|
|
342
|
+
if (overflow) return;
|
|
343
|
+
bytes += buf.length;
|
|
344
|
+
if (bytes > FONT_SCAN_MAX_BYTES) { overflow = true; return; }
|
|
345
|
+
chunks.push(buf);
|
|
346
|
+
});
|
|
347
|
+
child.on('error', (err) => finish({ ok: false, stdout: '', error: String((err && err.message) || err) }));
|
|
348
|
+
child.on('close', (code) => {
|
|
349
|
+
if (overflow) { finish({ ok: false, stdout: '', error: 'too-large' }); return; }
|
|
350
|
+
finish({ ok: code === 0, stdout: Buffer.concat(chunks).toString('utf8'), error: code === 0 ? '' : 'exit ' + code });
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** 跑**一条**来源:命令失败 / 输出为空 / 解析抛错都返 `null`(调用方只关心"拿到名字没有")。 */
|
|
356
|
+
async function runSource(source, run, now) {
|
|
357
|
+
let res;
|
|
358
|
+
try { res = await run(source.cmd, source.args); } catch (err) {
|
|
359
|
+
res = { ok: false, stdout: '', error: String((err && err.message) || err) };
|
|
360
|
+
}
|
|
361
|
+
if (!res || !res.ok || !res.stdout) return null;
|
|
362
|
+
let families;
|
|
363
|
+
try { families = source.parse(res.stdout); } catch { return null; }
|
|
364
|
+
return { source: source.source, families, at: now() };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 扫一次本机字体。
|
|
369
|
+
*
|
|
370
|
+
* 顺序(每一步都是"上一步真的没拿到名字"才走):
|
|
371
|
+
* ① **权威来源**(可多条)**并行**跑,名字取**并集** —— macOS 那条"本地化名 + 规范名"的并集
|
|
372
|
+
* 就是两种写法都能选到的原因(见 `CORETEXT_LIST_ARGS` 上面那段);
|
|
373
|
+
* ② Windows 注册表(**非**权威:值名不是族名)⇒ 标 `approximate`;
|
|
374
|
+
* ③ 按字体文件名推 ⇒ 标 `approximate`。
|
|
375
|
+
* @param {object} deps 依赖注入面(全部可省;缺省走真实环境)
|
|
376
|
+
* @param {string} [deps.platform] 'darwin' | 'win32' | 'linux' | …
|
|
377
|
+
* @param {(cmd:string, args:string[]) => Promise<{ok:boolean, stdout:string, error?:any}>} [deps.run] 可注入;默认真 spawn
|
|
378
|
+
* @param {string} [deps.home] 家目录(默认 `homedir()`;守卫喂夹具)
|
|
379
|
+
* @param {string[]} [deps.dirs] 降级路径要扫的目录(默认按平台 + 家目录推;守卫喂夹具目录)
|
|
380
|
+
* @param {() => number} [deps.now] 时钟(默认 `Date.now`)
|
|
381
|
+
* @param {(name:string) => (string|undefined)} [deps.keyOf] 共享内核的名字校验面(`systemFontKeyOf`;
|
|
382
|
+
* 纯函数,本文件**没有**模块级默认 —— 调用点必须给,缺了 `normalizeFamilies` 抛错)
|
|
383
|
+
* @returns `{ fonts, source, approximate, scannedAt }` —— `fonts` 可能为空 = 真的一个都没读到
|
|
384
|
+
*/
|
|
385
|
+
async function collectSystemFonts(deps) {
|
|
386
|
+
const d = deps && typeof deps === 'object' ? deps : {};
|
|
387
|
+
const platform = typeof d.platform === 'string' ? d.platform : process.platform;
|
|
388
|
+
const run = typeof d.run === 'function' ? d.run : defaultRun;
|
|
389
|
+
const home = typeof d.home === 'string' && d.home ? d.home : homedir();
|
|
390
|
+
const now = typeof d.now === 'function' ? d.now : Date.now;
|
|
391
|
+
// 共享内核的名字校验面(纯函数)由调用方传入;本文件零主包 specifier ⇒ 没有模块级默认可退
|
|
392
|
+
// (normalizeFamilies 缺 face 会显式抛错)。
|
|
393
|
+
const keyOf = typeof d.keyOf === 'function' ? d.keyOf : undefined;
|
|
394
|
+
|
|
395
|
+
const authoritative = await Promise.all(
|
|
396
|
+
authoritativeSources(platform).map((s) => runSource(s, run, now)));
|
|
397
|
+
const merged = [];
|
|
398
|
+
const used = [];
|
|
399
|
+
let scannedAt = 0;
|
|
400
|
+
for (const r of authoritative) {
|
|
401
|
+
if (!r) continue;
|
|
402
|
+
merged.push(...r.families);
|
|
403
|
+
used.push(r.source);
|
|
404
|
+
if (r.at > scannedAt) scannedAt = r.at;
|
|
405
|
+
}
|
|
406
|
+
const fonts = normalizeFamilies(merged, keyOf);
|
|
407
|
+
if (fonts.length) {
|
|
408
|
+
return { fonts, source: used.join('+'), approximate: false, scannedAt: scannedAt || now() };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ② 非权威来源(Windows 注册表):值名是"字体名"不是族名 ⇒ 标推测。
|
|
412
|
+
for (const s of fallbackSources(platform)) {
|
|
413
|
+
const r = await runSource(s, run, now);
|
|
414
|
+
if (!r) continue;
|
|
415
|
+
const f = normalizeFamilies(r.families, keyOf);
|
|
416
|
+
if (f.length) return { fonts: f, source: r.source, approximate: true, scannedAt: r.at };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ③ 降级:按文件名推(同样**标明是推测**)。
|
|
420
|
+
const scanDirs = Array.isArray(d.dirs) ? d.dirs : fontDirs(platform, home);
|
|
421
|
+
const files = collectFontFiles(scanDirs, 0, []);
|
|
422
|
+
return {
|
|
423
|
+
fonts: normalizeFamilies(parseFontFileNames(files), keyOf),
|
|
424
|
+
source: 'file-names', approximate: true, scannedAt: now(),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** 注入契约(P2-7A / ADR-0013 D12):本族从 `c` 消费的**必需**键 —— 门面/组件行调用点必须逐键给到(判据 test/verify-inject-contracts.mjs 双向对账)。 */
|
|
429
|
+
// 四键都是**运行期实例**(清理句柄 / 路径前缀 / 缓存路径 / 日志),调用点 = `packages/fonts/lib/index.js`,
|
|
430
|
+
// 由主包 `host.keys('fonts')` 一次给全;`schema` / `http` 是共享内核的**纯符号面**,
|
|
431
|
+
// 由同一个调用点从 `ctx.wallpaperEngineHost` 显式传下(见文件头)—— 本文件零主包 specifier。
|
|
432
|
+
export const DEPS = ['disposers', 'base', 'cachePath', 'log', 'schema', 'http'];
|
|
433
|
+
/** 允许缺席的键:真实调用点一个都不传,代码各有默认实现(守卫/测试用它们换掉平台、目录与子进程)。 */
|
|
434
|
+
export const OPTIONAL_DEPS = ['platform', 'home', 'fontDirs', 'runFontCommand'];
|
|
435
|
+
|
|
436
|
+
export function registerSystemFontsRoutes(webServer, c) {
|
|
437
|
+
// 后四个是**可选**注入点:真实调用一个都不传(各有默认实现),守卫用它们换掉平台、目录与
|
|
438
|
+
// 子进程 —— 于是本族**不 spawn 真进程**也能判(这是本文件唯一能跨平台判的那一半)。
|
|
439
|
+
const { disposers, base: BASE, cachePath, log, schema, http, platform, home, fontDirs, runFontCommand } = c;
|
|
440
|
+
// `keyOf` 是**纯函数面**(`c.schema.systemFontKeyOf`),随 deps 传进 `collectSystemFonts`。
|
|
441
|
+
const deps = { platform, home, dirs: fontDirs, run: runFontCommand, keyOf: schema.systemFontKeyOf };
|
|
442
|
+
|
|
443
|
+
/** 内存缓存 `{ fonts, source, approximate, scannedAt }` —— 进程内共享,重启由落盘那份接上。 */
|
|
444
|
+
let mem = null;
|
|
445
|
+
/** 在途扫描:并发合并(多个窗口同时开设置页也只扫一次)。 */
|
|
446
|
+
let inflight = null;
|
|
447
|
+
|
|
448
|
+
const fresh = (v) => Boolean(v) && Array.isArray(v.fonts)
|
|
449
|
+
&& Date.now() - Number(v.scannedAt || 0) < SYSTEM_FONTS_TTL_MS;
|
|
450
|
+
|
|
451
|
+
function readDisk() {
|
|
452
|
+
try {
|
|
453
|
+
const o = JSON.parse(readFileSync(cachePath(), 'utf8'));
|
|
454
|
+
// 口径版本不符(含没有 `v` 的老缓存)⇒ 当没有缓存:TTL 有一周,靠"过期"来生效太慢。
|
|
455
|
+
if (o && o.v === SYSTEM_FONTS_CACHE_VERSION && Array.isArray(o.fonts) && Number.isFinite(o.scannedAt)) return o;
|
|
456
|
+
} catch { /* 没有 / 读不懂都当"没有" —— 缓存不是真源 */ }
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function writeDisk(v) {
|
|
461
|
+
try {
|
|
462
|
+
const file = cachePath();
|
|
463
|
+
const tmp = file + '.tmp';
|
|
464
|
+
writeFileSync(tmp, JSON.stringify(Object.assign({ v: SYSTEM_FONTS_CACHE_VERSION }, v)));
|
|
465
|
+
renameSync(tmp, file);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
// 写不进缓存不影响这次应答;留个痕就够(下次仍能重扫)。
|
|
468
|
+
try { log('system-fonts 缓存写入失败:' + (err && err.message ? err.message : err)); } catch { /* ignore */ }
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function scan() {
|
|
473
|
+
if (!inflight) {
|
|
474
|
+
inflight = collectSystemFonts(deps).then((got) => {
|
|
475
|
+
mem = got;
|
|
476
|
+
if (got.fonts.length) writeDisk(got); // 空清单不落盘:下次一问就重扫(与 star-count 同口径)
|
|
477
|
+
return got;
|
|
478
|
+
}).catch((err) => {
|
|
479
|
+
const why = String((err && err.message) || err);
|
|
480
|
+
try { log('system-fonts 扫描失败:' + why); } catch { /* ignore */ }
|
|
481
|
+
return { fonts: [], source: 'none', approximate: false, scannedAt: 0, error: why };
|
|
482
|
+
}).finally(() => { inflight = null; });
|
|
483
|
+
}
|
|
484
|
+
return inflight;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
disposers.push(webServer.register({
|
|
488
|
+
kind: 'exact',
|
|
489
|
+
path: `${BASE}/system-fonts`,
|
|
490
|
+
handler: async (req, res) => {
|
|
491
|
+
// 薄别名:保留调用点写法(只有 200 + body),实现来自共享内核的 `c.http.sendJson`
|
|
492
|
+
// (主包 lib/json-response.js 的唯一实现)。
|
|
493
|
+
// 缓存由**本路由**管(TTL 在上游),别让浏览器再叠一层不可控的缓存 ⇒ no-store。
|
|
494
|
+
const reply = (body) => http.sendJson(res, 200, body);
|
|
495
|
+
if ((req.method || 'GET').toUpperCase() !== 'GET') { res.statusCode = 405; res.end(); return; }
|
|
496
|
+
let force;
|
|
497
|
+
try { force = new URL(req.url, 'http://localhost').searchParams.get('refresh') === '1'; } catch { force = false; }
|
|
498
|
+
|
|
499
|
+
const cached = mem || readDisk();
|
|
500
|
+
if (cached) mem = cached;
|
|
501
|
+
if (!force && fresh(cached)) {
|
|
502
|
+
reply({ ok: true, fonts: cached.fonts, approximate: cached.approximate === true, source: cached.source || '', scannedAt: cached.scannedAt, stale: false });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// 过期但手里有旧值:**先回旧值**(标 stale),同时后台重扫 —— 面板不因一次 10s 扫描卡住。
|
|
506
|
+
if (cached && Array.isArray(cached.fonts) && cached.fonts.length) {
|
|
507
|
+
scan().catch(() => { /* 后台重扫失败不影响这次应答 */ });
|
|
508
|
+
reply({ ok: true, fonts: cached.fonts, approximate: cached.approximate === true, source: cached.source || '', scannedAt: cached.scannedAt, stale: true });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const got = await scan();
|
|
512
|
+
reply({
|
|
513
|
+
ok: got.fonts.length > 0,
|
|
514
|
+
fonts: got.fonts,
|
|
515
|
+
approximate: got.approximate === true,
|
|
516
|
+
source: got.source || '',
|
|
517
|
+
scannedAt: got.scannedAt || 0,
|
|
518
|
+
stale: false,
|
|
519
|
+
error: got.fonts.length ? undefined : (got.error || 'no-fonts'),
|
|
520
|
+
});
|
|
521
|
+
},
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export {
|
|
526
|
+
collectSystemFonts, normalizeFamilies, familyFromFileName, defaultRun,
|
|
527
|
+
parseMacFonts, parseFcList, parseLineFamilies, parseWindowsFontRegistry, parseFontFileNames,
|
|
528
|
+
authoritativeSources,
|
|
529
|
+
SYSTEM_FONTS_TTL_MS, SYSTEM_FONTS_CACHE_VERSION, FONT_SCAN_TIMEOUT_MS,
|
|
530
|
+
};
|