/* font_theme.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ #include "meow_launcher_internal.h" /* M2.6 字重结构 + M2.8 三级定案(FONT_SYSTEM):卡内字体 = * <卡根>/MeowOS/fonts///(两级,向后兼容)或 * <卡根>/MeowOS/fonts////(三级,tier=common/gbk/full)。 * 每个 preset = 一套 /[/](如 misans/regular 或 source_han_sans_cn/regular/gbk)。 * 值 = 卡根相对路径 MeowOS/fonts//[/]/font.png;family/weight 单列供设置页 * 「family→字重」子菜单分组与 meow_font_effective_paths_tiered 按 family+weight 选库。 */ const char *font_presets[FONT_PRESETS_MAX]; /* SC_PRESET 值 = 卡根相对路径 */ const char *font_preset_labels[FONT_PRESETS_MAX]; /* SC_PRESET 中文标签(含字重+档位大小标记) */ /* M2.5x/M2.8 字体档位:每套的档("common"/"gbk"/"full";两级取 font.json charset,缺省 * "common";三级取 tier 目录名,权威)与 glyph_count(信息用)。供档位分层装载 * (meow_font_effective_paths_tiered)与设置页标注/选档。 */ const char *font_preset_tiers[FONT_PRESETS_MAX]; static int font_preset_glyphs[FONT_PRESETS_MAX]; const char *font_preset_fams[FONT_PRESETS_MAX]; /* family slug(目录一级) */ const char *font_preset_wts[FONT_PRESETS_MAX]; /* weight slug(目录二级) */ const char *font_preset_names[FONT_PRESETS_MAX]; /* font.json name(family 显示名) */ static char font_preset_val_buf[FONT_PRESETS_MAX][320]; /* 值持久存储(指向上面指针数组) */ static char font_preset_label_buf[FONT_PRESETS_MAX][96]; /* 标签持久存储(含字重+档位标记) */ static char font_preset_tier_buf[FONT_PRESETS_MAX][8]; /* charset 持久存储 */ static char font_preset_fam_buf[FONT_PRESETS_MAX][64]; /* family 持久存储 */ static char font_preset_wt_buf[FONT_PRESETS_MAX][32]; /* weight 持久存储 */ static char font_preset_name_buf[FONT_PRESETS_MAX][64]; /* name 持久存储 */ int font_presets_n = 0; /* 运行时动态计数(默认=1,仅"默认字体") */ int mfont_ready = 0; int mfont_forced = 0; /* font.file 变更→强制真实(un)load,跳过 SDK 复用捷径 */ /* fb_open 后调用:设置字库绘制目标为当前双缓冲(buffer0 起点 = fb 偏移 0) */ void meow_font_bind_fb(void) { meow_font_set_fb(fb_xres, fb_yres, fb_line); } /* 把 font.file 存的「卡根相对/绝对」路径解析为设备绝对路径。 * 相对路径以 /usr/data/mnt/sd_0/ 为卡根(与 MEOWAPPS 同源);绝对路径原样。 * 返回 0 成功,-1 参数无效。 */ int meow_font_resolve_path(const char *ref, char *out, int olen) { if (!ref || !ref[0]) return -1; if (ref[0] == '/') { snprintf(out, olen, "%s", ref); return 0; } snprintf(out, olen, "/usr/data/mnt/sd_0/%s", ref); return 0; } /* 解析 font.file 指向的 png + 派生同目录 idx(font.png → font.idx)。 * 仅当指向的 png 存在(access R_OK)返回 0;否则 -1(交由回退链)。 * 返回 0:out_png/out_idx 已填设备绝对路径。 */ static int meow_font_resolve_selected(char *out_png, int png_len, char *out_idx, int idx_len) { char ref[MEOW_SET_MAX_LEN]; if (meow_settings_get_str("font.file", ref, sizeof ref) != 0) return -1; if (meow_font_resolve_path(ref, out_png, png_len) != 0) return -1; if (access(out_png, R_OK) != 0) return -1; /* 指向不存在 → 回退卡默认/固件 */ snprintf(out_idx, idx_len, "%s", out_png); /* 同目录 font.idx(标准三件套布局) */ size_t n = strlen(out_idx); if (n > 4 && !strcmp(out_idx + n - 4, ".png")) { out_idx[n-4] = '.'; out_idx[n-3] = 'i'; out_idx[n-2] = 'd'; out_idx[n-1] = 'x'; out_idx[n] = 0; } return 0; } /* 尝试加载字库;成功返回 1。优先级(M2.5x,font.load_full 已接入): * 0) 生效档 = font.load_full ? gbk : common(launcher 自身 UI 跟随系统大字库开关); * 1) 生效字体(tier 解析,tier0 即 font.file 所选;相对路径按卡根解析); * 2) 卡 MeowOS/fonts/default/regular/font.png(默认字库); * 3) 固件 /usr/share/meowos/font/font.png。 * A-1/A-2:SDK meow_ui_init() 已用同一 meow_font.o 加载过(可能成功也可能未成功)。 * 当字体指向「默认卡字库」或未设置时复用 SDK 已加载状态,避免重复 unload+reload; * 当字体指向自定义字库时必须显式 load(SDK 只加载默认,不读 font.file)。 * mfont_forced=1(font.file/font.load_full 变更触发)时无条件真实 reload,跳过上述捷径。 * 全失败 → mfont_ready=0,ui_draw 回退 5x7 点阵(系统不死)。 */ int meow_font_ensure(void) { if (mfont_ready && !mfont_forced) return 1; mfont_forced = 0; char fpng[400], fidx[400], fam[96]; int tier = meow_font_effective_tier(MEOW_FONT_TIER_COMMON); /* launcher 自身 UI 跟随 load_full */ int have_sel = (meow_font_effective_paths_tiered(tier, fpng, sizeof fpng, fidx, sizeof fidx, fam, sizeof fam) == 0); /* SDK 已加载且所选字库为「默认卡字库」(或未设置)→ 复用,避免重复解码。 */ if (meow_gfx_mfont_ready() && (!have_sel || !strcmp(fpng, UI_FONT_PNG_CARD))) { mfont_ready = 1; logmsg("meow_font: reuse sdk-loaded font"); return 1; } /* 生效字体优先(tier=common 时即 font.file;load_full → 大字库 gbk 档)。 */ if (have_sel && meow_font_load(fpng, fidx) == 0) { mfont_ready = 1; logmsg("meow_font: loaded %s (%d/%d sizes), tier=%d", fpng, meow_font_has_size(28), meow_font_has_size(20), tier); return 1; } if (have_sel) logmsg("meow_font: selected font %s missing, fallback default", fpng); /* 系统日志 */ /* 卡默认 → 固件。 */ if (meow_font_load(UI_FONT_PNG_CARD, UI_FONT_IDX_CARD) == 0 || meow_font_load(UI_FONT_PNG_SYS, UI_FONT_IDX_SYS) == 0) { mfont_ready = 1; logmsg("meow_font: loaded fallback (%d/%d sizes)", meow_font_has_size(28), meow_font_has_size(20)); return 1; } logmsg("meow_font: all font sources missing, using 5x7 bitmap"); /* WARN:字库不可用 */ return 0; } /* 从 MeowOS/fonts///font.png 绝对路径提 family(第一级目录名)。 * 如 .../misans/regular/font.png → "misans"、.../default/regular/font.png → "default"; * 结构异常/找不到前缀 → 回退 "default"。 */ static void font_family_from_png(const char *png, char *out, int olen) { const char *prefix = "/MeowOS/fonts/"; const char *p = png ? strstr(png, prefix) : NULL; if (!p) { snprintf(out, olen, "default"); return; } const char *q = p + strlen(prefix); const char *slash = strchr(q, '/'); int n = slash ? (int)(slash - q) : (int)strlen(q); if (n <= 0 || n >= olen) { snprintf(out, olen, "default"); return; } memcpy(out, q, (size_t)n); out[n] = 0; } /* 同路径同时提取 family + weight(两级目录名)。weight 缺省 "regular"。 */ void font_family_weight_from_png(const char *png, char *fam, int flen, char *wt, int wlen) { const char *prefix = "/MeowOS/fonts/"; const char *p = png ? strstr(png, prefix) : NULL; if (!p) { snprintf(fam, flen, "default"); snprintf(wt, wlen, "regular"); return; } const char *q = p + strlen(prefix); const char *s1 = strchr(q, '/'); if (!s1) { snprintf(fam, flen, "%s", q); snprintf(wt, wlen, "regular"); return; } int nf = (int)(s1 - q); if (nf > 0 && nf < flen) { memcpy(fam, q, (size_t)nf); fam[nf] = 0; } else snprintf(fam, flen, "default"); const char *s2 = strchr(s1 + 1, '/'); int nw = s2 ? (int)(s2 - (s1 + 1)) : (int)strlen(s1 + 1); if (nw > 0 && nw < wlen) { memcpy(wt, s1 + 1, (size_t)nw); wt[nw] = 0; } else snprintf(wt, wlen, "regular"); } /* 求「当前生效字体」的 png/idx 绝对路径 + family,供 launch_app 注入 MEOW_FONT_PNG/IDX/APP。 * 优先级(与 meow_font_ensure 一致):font.file(resolve_selected)→ 卡默认 MeowOS/fonts/default。 * family 取自 MeowOS/fonts///font.png 的第一级目录名;无法判定则 "default"。 * 返回 0 成功(out_png/out_idx/out_family 已填),-1 无卡/无可用字体(调用方跳过注入, * SDK 自身会回退卡默认→固件→点阵)。 */ static int meow_font_effective_paths(char *out_png, int plen, char *out_idx, int ilen, char *out_family, int flen) { char fpng[400], fidx[400]; if (meow_font_resolve_selected(fpng, sizeof fpng, fidx, sizeof fidx) == 0) { snprintf(out_png, plen, "%s", fpng); snprintf(out_idx, ilen, "%s", fidx); font_family_from_png(fpng, out_family, flen); return 0; } /* 回退卡默认 MeowOS/fonts/default */ if (access(UI_FONT_PNG_CARD, R_OK) == 0) { snprintf(out_png, plen, "%s", UI_FONT_PNG_CARD); snprintf(out_idx, ilen, "%s", UI_FONT_IDX_CARD); snprintf(out_family, flen, "default"); return 0; } return -1; } /* 档位 → font.json charset 字符串名(common/gbk/full)。 */ static const char *font_tier_charset(int tier) { if (tier >= MEOW_FONT_TIER_FULL) return "full"; if (tier >= MEOW_FONT_TIER_GBK) return "gbk"; return "common"; } /* 生效档:app 声明档(0/1/2,越界 clamp)合并「加载大字库」开关(font.load_full)。 * 开 → 强制 ≥GBK(gbk≈20k 档大字库);关(默认)→ 保持 app 声明(缺省 common≈4k)。 * 断言:0<=返回<=2。 */ int meow_font_effective_tier(int app_tier) { if (app_tier < MEOW_FONT_TIER_COMMON) app_tier = MEOW_FONT_TIER_COMMON; if (app_tier > MEOW_FONT_TIER_FULL) app_tier = MEOW_FONT_TIER_FULL; int lf = 0; if (meow_settings_get_switch("font.load_full", &lf) == 0 && lf) if (app_tier < MEOW_FONT_TIER_GBK) app_tier = MEOW_FONT_TIER_GBK; return app_tier; } /* 【tier 选择入口,M2.5x 已实现】按字体档位取「当前生效字体」png/idx,供 launch_app 注入 * MEOW_FONT_PNG/IDX/APP(配合 MEOW_FONT_TIER)。口径(MEOWOS_SPEC_REF §5.2/§5.7.8): * - 生效档 = max(app 声明档, font.load_full ? GBK : COMMON):系统「加载大字库」开 → * 强制 ≥gbk; * - tier<=common → 走既有 meow_font_effective_paths(font.file/卡默认/固件); * - tier>0 → 在已扫描的 preset(两级或三级,值路径 = MeowOS/fonts//[/]/font.png) * 里找档位匹配(gbk 请求接受 gbk/full;full 请求接受 full)的那套,以其 font.png/idx 绝对路径返回 * (family = 一级目录名)。 * - M2.6 字重:优先选「与用户当前 font.file 同 family(其次同 weight)」的大字库,避免 * 大字库跳到另一家字体(三级同 family 下按 tier 找 gbk/full,见下);找不到匹配 → * 回退 meow_font_effective_paths(避免点到固件小库)。 * 返回 0 成功(out_png/out_idx/out_family 已填),-1 无可用。 */ int meow_font_effective_paths_tiered(int tier, char *out_png, int plen, char *out_idx, int ilen, char *out_family, int flen) { int eff = meow_font_effective_tier(tier); /* 合并 font.load_full:开 → 强制 ≥GBK */ if (eff <= MEOW_FONT_TIER_COMMON) return meow_font_effective_paths(out_png, plen, out_idx, ilen, out_family, flen); /* 大字库:从扫描结果选 charset 匹配的一套。 * 打分(档位主导 ×100):gbk 请求 → gbk=200 > full=100;full 请求 → full=200 > gbk=100; * + 同 family +30(其次同 weight +10,M2.6)仅作同档位内 tie-break:大字库沿用用户所选 * family,但绝不因「同 family」而把 common 小库越档选成大字库。 */ const char *want = font_tier_charset(eff); char curfam[64] = "", curwt[32] = ""; { /* 用户当前 font.file 所选 family/weight(无法判定 → 空,不参与加成) */ char fpng[400], fidx[400]; if (meow_font_resolve_selected(fpng, sizeof fpng, fidx, sizeof fidx) == 0) font_family_weight_from_png(fpng, curfam, sizeof curfam, curwt, sizeof curwt); } char ref[320]; int found = -1, best_score = 0; for (int i = 0; i < font_presets_n; i++) { const char *cs = font_preset_tiers[i] ? font_preset_tiers[i] : "common"; /* 档位优先(×100 主导):gbk 请求 → gbk=200 > full=100;full 请求 → full=200 > gbk=100。 * M2.6:同 family +30、同 weight +10 仅作同档位内 tie-break(不越档选小库)。 */ int score = 0; if (!strcmp(cs, want)) score = 200; else if (!strcmp(cs, "full") || !strcmp(cs, "gbk")) score = 100; /* 降级 */ if (curfam[0] && font_preset_fams[i] && !strcmp(font_preset_fams[i], curfam)) score += 30; if (curfam[0] && curwt[0] && font_preset_wts[i] && !strcmp(font_preset_wts[i], curwt)) score += 10; if (score > best_score) { best_score = score; found = i; } } if (found >= 0) snprintf(ref, sizeof ref, "%s", font_presets[found]); if (found >= 0) { /* 相对路径按卡根解析为设备绝对路径(与 meow_font_effective_paths 同源)。 */ if (meow_font_resolve_path(ref, out_png, plen) != 0) return -1; if (access(out_png, R_OK) != 0) goto fallback; snprintf(out_idx, ilen, "%s", out_png); size_t n = strlen(out_idx); if (n > 4 && !strcmp(out_idx + n - 4, ".png")) { out_idx[n-4] = '.'; out_idx[n-3] = 'i'; out_idx[n-2] = 'd'; out_idx[n-1] = 'x'; out_idx[n] = 0; } font_family_from_png(out_png, out_family, flen); return 0; } fallback: /* 无匹配大字库/不可读 → 回退全局(font.file 所选;其可能是大字库或默认)。 */ return meow_font_effective_paths(out_png, plen, out_idx, ilen, out_family, flen); } /* M2.5x 多字体:从 settings 读 display.multi_font + 6 类 font.class_*,把各自相对路径按卡根 * 解析为绝对路径,去重装载到 meow_font 槽位(同 family 去重 & ≤4 套上限由 * meow_font_config_multifont 统一处理)。仅当全局字库已装载(mfont_ready / slot0)时生效; * 关或未装载 → 单字体(on=0),零额外开销。调用时机:启动 meow_font_ensure() 之后,以及 * 设置页变更 display.multi_font / font.class_* / font.file。 */ void meow_font_reload_multifont(void) { static const char *cls_keys[MEOW_FCLS_COUNT] = { "font.class_cjk", "font.class_cjk_sym", "font.class_num", "font.class_latin", "font.class_latin_sym", "font.class_other" }; int on = 0; meow_settings_get_switch("display.multi_font", &on); char gpng[400], gidx[400], gfam[96]; /* 全局跟生效档(合并 font.load_full → 大字库),与 meow_font_ensure 同源,避免槽0实际装载与 * 此处理解不一致(多字体去重以全局 png 为基准)。 */ if (meow_font_effective_paths_tiered(meow_font_effective_tier(MEOW_FONT_TIER_COMMON), gpng, sizeof gpng, gidx, sizeof gidx, gfam, sizeof gfam) != 0) return; /* 无全局字体:无法多字体 */ /* 各类候选(静态缓冲保活;config_multifont 同步读取,不持指针) */ static char cpng[MEOW_FCLS_COUNT][400], cidx[MEOW_FCLS_COUNT][400]; meow_font_class_cfg cfg[MEOW_FCLS_COUNT]; memset(cfg, 0, sizeof cfg); for (int c = 0; c < MEOW_FCLS_COUNT; c++) { char ref[MEOW_SET_MAX_LEN]; if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) == 0 && ref[0]) { char abs[400]; if (meow_font_resolve_path(ref, abs, sizeof abs) == 0 && access(abs, R_OK) == 0) { snprintf(cpng[c], sizeof cpng[c], "%s", abs); snprintf(cidx[c], sizeof cidx[c], "%s", abs); size_t n = strlen(cidx[c]); if (n > 4 && !strcmp(cidx[c] + n - 4, ".png")) { cidx[c][n-4]='.'; cidx[c][n-3]='i'; cidx[c][n-2]='d'; cidx[c][n-1]='x'; cidx[c][n]=0; } cfg[c].png = cpng[c]; cfg[c].idx = cidx[c]; } else { /* M2.58:类别字体配置了但缺失 → 该类别回退全局,并记系统 WARN。 */ logmsg("multi_font: class %s font %s missing, fallback global", cls_keys[c], ref); } } } int distinct = 0, truncated = 0; if (meow_font_config_multifont(on, gpng, gidx, cfg, &distinct, &truncated) != 0) logmsg("multi_font: config failed -> single-font"); else if (truncated) logmsg("multi_font: WARN >%d 套不同字体(会增内存),已按上限截断", distinct); else if (on) logmsg("multi_font: enabled, %d distinct fonts (global=%s)", distinct, gfam); } /* ================================================================== */ /* M2.5x 多字体库扫描 + M2.6 字重结构 / preset 动态化 */ /* 字体目录 = <卡根>/MeowOS/fonts///,每套含三件套: */ /* font.png(预渲染灰度 PNG,bit depth 4|8)+ font.idx(MFNI v2 索引) */ /* + font.json(元数据 + 完整性校验)。缺任一件 → 该套不进列表 + 日志。 */ /* font.json schema(定版): */ /* { */ /* "family": "家族标识(同目录名/family)", // 信息用,不参与校验 */ /* "name": "下拉显示名", // 缺则回退目录名 */ /* "weight": "regular", // 字重 slug(缺省 regular) */ /* "glyph_count": 10000, // 信息用 */ /* "charset": "common|gbk|full", // 档位(M2.5x,驱动按需加载) */ /* "size_list": [28,20], // 信息用 */ /* "bit_depth": 4, // 信息用(4|8) */ /* "sha256_png": "<64hex>", // font.png 校验 */ /* "sha256_idx": "<64hex>", // font.idx 校验 */ /* "source": "...", // 可选:来源 */ /* "license": "..." // 可选:许可 */ /* } */ /* 校验:sha256 不匹配仅 WARN 不硬失败(仍可选);缺三件套或 font.json */ /* 解析不出 name → 跳过 + 日志。仅启动/进设置页扫描一次(勿放热路径)。 */ /* ================================================================== */ /* 计算文件 sha256(走 busybox/宿主 sha256sum;输出小写 64hex)。返回 0 成功。 */ static int font_sha256_file(const char *path, char *out, int olen) { if (!out || olen <= 0) return -1; out[0] = 0; /* 单引号转义防 shell 注入(路径含 ' 时安全;转义后超长则截断 → sha256sum 失败走 WARN) */ char esc[1024]; int j = 0; for (const char *p = path; *p && j < (int)sizeof esc - 4; ) { if (*p == '\'') { memcpy(esc + j, "'\\''", 4); j += 4; p++; } else esc[j++] = *p++; } esc[j] = 0; char cmd[1080]; snprintf(cmd, sizeof cmd, "sha256sum '%s' 2>/dev/null", esc); /* M2.84-bugfix:禁止裸 popen+fgets——sha256sum 在卡慢/挂起时可能长时间不返回, * fgets 会阻塞在 pipe_wait(主循环卡死、电源键无响应)。改用有界 popen_read_tmo * (O_NONBLOCK + poll + 超时,超时 SIGKILL 子进程),绝不阻塞主循环。 */ char *o = popen_read_tmo(1500, cmd); if (!o) return -1; int i = 0, k = 0; while (o[i] == ' ' || o[i] == '\t') i++; /* 跳前导空白 */ while (o[i] && k < 64 && k + 1 < olen) { char c = o[i++]; if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) out[k++] = c; else break; } out[k] = 0; free(o); return (k == 64) ? 0 : -1; } /* 比较两个 sha256 十六进制串(大小写不敏感)。均为 64hex 才比对,否则视为不匹配。 */ static int font_sha_eq(const char *a, const char *b) { if (!a || !b || strlen(a) < 64 || strlen(b) < 64) return 0; for (int i = 0; i < 64; i++) { char ca = a[i], cb = b[i]; if (ca >= 'A' && ca <= 'F') ca += 32; if (cb >= 'A' && cb <= 'F') cb += 32; if (ca != cb) return 0; } return 1; } /* 解析 font.json:提取 name / sha256_png / sha256_idx / weight(M2.6,缺省 "regular")。 * 文件缺失或解析不出则留空(name 空 → 调用方回退目录名;weight 空 → 回退 "regular")。 */ static void meow_font_json_probe(const char *json_path, char *name, int nlen, char *sha_png, int slen, char *sha_idx, int xlen, char *charset, int clen, int *glyph_count, char *weight, int wlen) { name[0] = 0; sha_png[0] = 0; sha_idx[0] = 0; if (charset) charset[0] = 0; if (glyph_count) *glyph_count = 0; if (weight) weight[0] = 0; FILE *f = fopen(json_path, "r"); if (!f) return; char buf[256]; while (fgets(buf, sizeof buf, f)) { if (!name[0]) json_str(buf, "name", name, nlen); if (!sha_png[0]) json_str(buf, "sha256_png", sha_png, slen); if (!sha_idx[0]) json_str(buf, "sha256_idx", sha_idx, xlen); /* M2.5x 字体档位:charset(common|gbk|full)+ glyph_count(信息用,驱动档位分层/标注)。 */ if (charset && !charset[0]) { char v[8] = ""; if (json_str(buf, "charset", v, sizeof v) > 0 && v[0]) snprintf(charset, clen, "%s", v); } if (glyph_count && *glyph_count == 0) { char v[16] = ""; if (json_str(buf, "glyph_count", v, sizeof v) > 0 && v[0]) *glyph_count = atoi(v); } /* M2.6 字重:weight(regular|bold|italic|…),缺省 "regular"。 */ if (weight && !weight[0]) { char v[32] = ""; if (json_str(buf, "weight", v, sizeof v) > 0 && v[0]) snprintf(weight, wlen, "%s", v); } } fclose(f); } /* 字重 slug → 显示名(首字母大写、连字符/下划线转空格:regular→Regular、bold→Bold、italic→Italic)。 */ static void font_weight_display(const char *weight, char *out, int olen) { const char *w = (weight && weight[0]) ? weight : "regular"; out[0] = 0; int o = 0, cap = 1; for (const char *p = w; *p && o < olen - 1; p++) { char c = *p; if (c == '_' || c == '-') { if (o < olen - 1) out[o++] = ' '; cap = 1; continue; } out[o++] = cap ? (char)((c >= 'a' && c <= 'z') ? c - 32 : c) : c; cap = 0; } out[o] = 0; } /* 档位大小标记(设置页字体选项标注):common≈4k / gbk≈20k / full。 * 由 font.json 的 charset + glyph_count 派生;缺 charset 回退 common。 */ static void font_tier_marker(const char *charset, int glyph_count, char *out, int olen) { const char *tier = (charset && charset[0]) ? charset : "common"; if (!strcmp(tier, "full")) { snprintf(out, olen, "full"); return; } if (glyph_count > 0) snprintf(out, olen, "%s≈%dk", tier, (glyph_count + 500) / 1000); else snprintf(out, olen, "%s", tier); } /* 探测一套件是否合法:三件套齐全 → 解析 label+档位+字重。 * dirpath = <卡根>/MeowOS/fonts//(两级)或 ...///(三级)。 * tier_dir:三级时传该 tier 目录名(common/gbk/full,权威档位);两级传 NULL(档位读 * font.json charset,缺省 common)。 * 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。 * 输出:label=显示名 ` (common≈4k)`(如 `MiSans Regular(common≈4k)`); * charset/glyph_count=档位信息;wtout=weight slug(缺省 "regular")。 */ static int meow_font_probe_weight(const char *dirpath, const char *family, const char *weight, const char *tier_dir, char *label, int llen, char *charset, int clen, int *glyph_count, char *wtout, int wtlen, char *nameout, int namelen) { char ppath[400], ipath[400], jpath[400]; snprintf(ppath, sizeof ppath, "%s/font.png", dirpath); snprintf(ipath, sizeof ipath, "%s/font.idx", dirpath); snprintf(jpath, sizeof jpath, "%s/font.json", dirpath); const int is3 = (tier_dir != NULL && tier_dir[0] != 0); const char *tsep = is3 ? "/" : ""; const char *tiernm = is3 ? tier_dir : ""; if (access(ppath, R_OK) != 0 || access(ipath, R_OK) != 0 || access(jpath, R_OK) != 0) { logmsg("fonts: skip '%s/%s%s%s' (缺 font.png/font.idx/font.json)", family, weight, tsep, tiernm); return 0; } char name[64] = "", jsha_png[80] = "", jsha_idx[80] = ""; char cs[8] = ""; int gc = 0; char wts[32] = ""; meow_font_json_probe(jpath, name, sizeof name, jsha_png, sizeof jsha_png, jsha_idx, sizeof jsha_idx, cs, sizeof cs, &gc, wts, sizeof wts); const char *wt = wts[0] ? wts : (weight ? weight : "regular"); /* 档位:三级定案 = tier 目录名(权威);两级(向后兼容)= font.json charset(缺省 common)。 */ const char *tier = is3 ? tier_dir : (cs[0] ? cs : "common"); if (wtout) snprintf(wtout, wtlen, "%s", wt); if (nameout) snprintf(nameout, namelen, "%s", name[0] ? name : family); /* 显示名 = ` (common≈4k)`(字重缺省 Regular) */ char wd[24]; font_weight_display(wt, wd, sizeof wd); char marker[24]; font_tier_marker(tier, gc, marker, sizeof marker); snprintf(label, llen, "%s %s(%s)", name[0] ? name : family, wd, marker); if (charset) snprintf(charset, clen, "%s", tier); if (glyph_count) *glyph_count = gc; /* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。 */ char apng[80] = "", aidx[80] = ""; if (font_sha256_file(ppath, apng, sizeof apng) != 0) logmsg("fonts: '%s/%s%s%s' sha256(png) 不可用,跳过校验", family, wt, tsep, tiernm); else if (jsha_png[0] && !font_sha_eq(apng, jsha_png)) logmsg("fonts: '%s/%s%s%s' WARN sha256_png 不匹配(仍可选)", family, wt, tsep, tiernm); if (font_sha256_file(ipath, aidx, sizeof aidx) != 0) logmsg("fonts: '%s/%s%s%s' sha256(idx) 不可用,跳过校验", family, wt, tsep, tiernm); else if (jsha_idx[0] && !font_sha_eq(aidx, jsha_idx)) logmsg("fonts: '%s/%s%s%s' WARN sha256_idx 不匹配(仍可选)", family, wt, tsep, tiernm); logmsg("fonts: add '%s/%s%s%s' (%s) tier=%s glyph_count=%d", family, wt, tsep, tiernm, label, tier, gc); return 1; } /* M2.58 字体索引:CSV 字段写入(始终双引号包裹,内嵌双引号加倍)。返回写入长度。 */ static int font_index_csvq(char *dst, int olen, const char *s) { int o = 0; if (olen <= 0) return 0; if (o < olen) dst[o++] = '"'; for (; *s && o < olen - 3; s++) { if (*s == '"') dst[o++] = '"'; if (o < olen - 2) dst[o++] = *s; } if (o < olen) dst[o++] = '"'; dst[o] = 0; return o; } /* 把当前 font_presets* 扫描结果落盘为 /MeowOS/config/fonts/index(覆盖)。 * 每套一条 `"family","weight","path","label","tier",glyph_count`;path=卡根相对 font.png。 * 目录不存在则 mkdir -p;无卡(mkdir/fopen 失败)→ 静默放弃(仅内存默认项)。 * 该文件是 SDK meow_font_get_available_count()/enumerate() 的持久数据源(规格 §5.7)。 */ static void meow_font_write_index(const char *card_root) { if (!card_root || !card_root[0]) return; char t1[360], t2[380], base[400], path[420]; snprintf(t1, sizeof t1, "%s/MeowOS", card_root); snprintf(t2, sizeof t2, "%s/MeowOS/config", card_root); snprintf(base, sizeof base, "%s/MeowOS/config/fonts", card_root); /* 无卡(card_root 父目录不存在 → ENOENT)→ 静默放弃:仅内存默认项,不刷系统日志。 */ if (mkdir(t1, 0755) != 0 && errno != EEXIST) { if (errno != ENOENT) logmsg("fonts: index mkdir fail %s", t1); return; } if (mkdir(t2, 0755) != 0 && errno != EEXIST) { logmsg("fonts: index mkdir fail %s", t2); return; } if (mkdir(base, 0755) != 0 && errno != EEXIST) { logmsg("fonts: index mkdir fail %s", base); return; } snprintf(path, sizeof path, "%s/index", base); FILE *f = fopen(path, "w"); if (!f) { logmsg("fonts: index fopen fail %s", path); return; } char line[1024]; for (int i = 0; i < font_presets_n; i++) { const char *fam = font_preset_fams[i] ? font_preset_fams[i] : "default"; const char *wt = font_preset_wts[i] ? font_preset_wts[i] : "regular"; const char *val = font_presets[i] ? font_presets[i] : "MeowOS/fonts/default/regular/font.png"; const char *lbl = font_preset_labels[i] ? font_preset_labels[i] : fam; const char *tier = font_preset_tiers[i] ? font_preset_tiers[i] : "common"; int o = 0; o = font_index_csvq(line, sizeof line, fam); line[o++] = ','; line[o] = 0; o += font_index_csvq(line + o, sizeof line - (size_t)o, wt); line[o++] = ','; line[o] = 0; o += font_index_csvq(line + o, sizeof line - (size_t)o, val); line[o++] = ','; line[o] = 0; o += font_index_csvq(line + o, sizeof line - (size_t)o, lbl); line[o++] = ','; line[o] = 0; o += font_index_csvq(line + o, sizeof line - (size_t)o, tier); line[o++] = ','; line[o] = 0; snprintf(line + o, sizeof line - (size_t)o, "%d\n", font_preset_glyphs[i]); fputs(line, f); } fclose(f); logmsg("fonts: index written (%d entries) -> %s", font_presets_n, path); } /* 把一个探测成功的套装登记为 preset i(值路径 = MeowOS/fonts//[/]/font.png)。 * tier 非空 → 三级路径;否则 → 两级路径(旧结构向后兼容)。返回 0 成功,-1 已满。 */ static int font_presets_add(const char *family, const char *weight, const char *tier, const char *label, const char *cs, int gc, const char *wt, const char *nm) { if (font_presets_n >= FONT_PRESETS_MAX) return -1; int i = font_presets_n; if (tier && tier[0]) snprintf(font_preset_val_buf[i], sizeof font_preset_val_buf[i], "%s/%s/%s/%s/font.png", MEOW_FONTS_CFG, family, weight, tier); else snprintf(font_preset_val_buf[i], sizeof font_preset_val_buf[i], "%s/%s/%s/font.png", MEOW_FONTS_CFG, family, weight); snprintf(font_preset_label_buf[i], sizeof font_preset_label_buf[i], "%s", label); snprintf(font_preset_tier_buf[i], sizeof font_preset_tier_buf[i], "%s", cs && cs[0] ? cs : "common"); snprintf(font_preset_fam_buf[i], sizeof font_preset_fam_buf[i], "%s", family); snprintf(font_preset_wt_buf[i], sizeof font_preset_wt_buf[i], "%s", wt && wt[0] ? wt : "regular"); snprintf(font_preset_name_buf[i], sizeof font_preset_name_buf[i], "%s", nm ? nm : family); font_presets[i] = font_preset_val_buf[i]; font_preset_labels[i] = font_preset_label_buf[i]; font_preset_tiers[i] = font_preset_tier_buf[i]; font_preset_fams[i] = font_preset_fam_buf[i]; font_preset_wts[i] = font_preset_wt_buf[i]; font_preset_names[i] = font_preset_name_buf[i]; font_preset_glyphs[i] = gc; font_presets_n++; return 0; } /* 扫描 /MeowOS/fonts/ 下字体,重建字体 preset 列表。 * 布局(FONT_SYSTEM 三级定案): * - 两级(向后兼容)://font.png → 每 (family,weight) 一套,tier 读 font.json * charset(缺省 common)。 * - 三级(定案):///(tier=common/gbk/full,每 tier 一套三件套)→ * 每 (family,weight,tier) 一套,tier=目录名(权威)。 * - 同一 (family,weight) 下:若 weight 根有 font.png(两级)则按两级;否则扫其 / 子目录。 * family 级 'default' 由索引 0(默认字体)单独处理,不进循环,避免重复。上限 FONT_PRESETS_MAX * (1 默认 + 卡内各套,按 (family,weight,tier) 一套一 preset 计数)。无卡/无目录时仅默认。 * * M2.58 释放语义(规格 §5.7 需求 4「扫→写 index→释放(或按需读 index)」结论): * - 持久来源 = /MeowOS/config/fonts/index(扫描后由 meow_font_write_index 落盘)。 * SDK 只读该文件,从不依赖本内存数组。 * - font_presets* / *_buf 均为「静态、有界(FONT_PRESETS_MAX=64)、非堆」数组:无内存泄漏, * 故无运行期 free;「释放」含义 = 两个扫描点(启动 / 进设置页)重建 + 每套固定上限防膨胀。 * - 设置字体列表(font.file / font.class_* 下拉、family→字重子菜单)在本视图内显示时用 * 该数组;离开字体列表视图(run_settings_page 退出)无需显式 free(静态数组),下次 * 进设置/重扫自动重建。 * - ⚠️ 未做「启动后即清空数组」:meow_font_effective_paths_tiered(launch_app 大字库档位 * 选库)与 meow_font_reload_multifont 在运行期持续读 font_presets_n,清空会造成大字库 * 档位退化(red line「保持档位/多字体行为」)。如需「零驻留」,须把这几个核心路径改为 * 直接读 index(较大重构,超出本批边界,留待后续)。 */ void meow_font_scan_presets_root(const char *card_root) { font_presets_n = 0; font_presets[0] = "MeowOS/fonts/default/regular/font.png"; /* 默认字库 = MeowOS/fonts/default/regular */ font_preset_labels[0] = font_preset_label_buf[0]; /* 指向缓冲(运行时派生含字重+大小标记) */ font_preset_fams[0] = font_preset_fam_buf[0]; font_preset_wts[0] = font_preset_wt_buf[0]; font_preset_names[0] = font_preset_name_buf[0]; /* 默认套也读 font.json 档位+字重(存在则跟真实档,缺则 common/regular);供分层装载/标注。 */ { char jp[400], cs[8] = ""; int gc = 0; char nm0[64] = "", sp0[80] = "", si0[80] = "", wt0[32] = ""; snprintf(jp, sizeof jp, "%s/%s/default/regular/font.json", card_root, MEOW_FONTS_CFG); meow_font_json_probe(jp, nm0, sizeof nm0, sp0, sizeof sp0, si0, sizeof si0, cs, sizeof cs, &gc, wt0, sizeof wt0); char mk[24]; font_tier_marker(cs, gc, mk, sizeof mk); char wd[24]; font_weight_display(wt0[0] ? wt0 : "regular", wd, sizeof wd); snprintf(font_preset_label_buf[0], sizeof font_preset_label_buf[0], "默认字体 %s(%s)", wd, mk); snprintf(font_preset_tier_buf[0], sizeof font_preset_tier_buf[0], "%s", cs[0]?cs:"common"); snprintf(font_preset_fam_buf[0], sizeof font_preset_fam_buf[0], "default"); snprintf(font_preset_wt_buf[0], sizeof font_preset_wt_buf[0], "%s", wt0[0]?wt0:"regular"); snprintf(font_preset_name_buf[0], sizeof font_preset_name_buf[0], "默认字体"); font_preset_tiers[0] = font_preset_tier_buf[0]; font_preset_glyphs[0] = gc; } font_presets_n = 1; char root[340]; snprintf(root, sizeof root, "%s/%s", card_root, MEOW_FONTS_CFG); DIR *d = opendir(root); if (d) { /* 无卡/无字库目录 → 跳过扫描,仅默认 */ struct dirent *de; while ((de = readdir(d)) && font_presets_n < FONT_PRESETS_MAX) { if (de->d_name[0] == '.') continue; if (!strcmp(de->d_name, "default")) continue; /* 默认字体由索引 0 单独处理 */ char fampath[400]; snprintf(fampath, sizeof fampath, "%s/%s", root, de->d_name); DIR *w = opendir(fampath); /* 一级:/ */ if (!w) continue; struct dirent *we; while ((we = readdir(w)) && font_presets_n < FONT_PRESETS_MAX) { if (we->d_name[0] == '.') continue; char dirpath[400], pngcheck[420]; snprintf(dirpath, sizeof dirpath, "%s/%s", fampath, we->d_name); snprintf(pngcheck, sizeof pngcheck, "%s/font.png", dirpath); char label[96] = "", cs[8] = ""; int gc = 0; char wt[32] = "", nm[64] = ""; if (access(pngcheck, R_OK) == 0) { /* 两级(旧结构向后兼容)://font.png,tier 读 font.json charset。 */ if (meow_font_probe_weight(dirpath, de->d_name, we->d_name, NULL, label, sizeof label, cs, sizeof cs, &gc, wt, sizeof wt, nm, sizeof nm) == 0) continue; /* 缺组件/损坏 → 跳过该套 */ font_presets_add(de->d_name, we->d_name, NULL, label, cs, gc, wt, nm); continue; } /* 三级定案:进入 // 逐套探测,tier=目录名(权威档位)。 */ DIR *t = opendir(dirpath); if (!t) continue; struct dirent *te; while ((te = readdir(t)) && font_presets_n < FONT_PRESETS_MAX) { if (te->d_name[0] == '.') continue; /* tier 仅接受 common/gbk/full(FONT_SYSTEM §1);杂散子目录不进列表。 */ if (strcmp(te->d_name, "common") && strcmp(te->d_name, "gbk") && strcmp(te->d_name, "full")) continue; char tpath[420]; snprintf(tpath, sizeof tpath, "%s/%s", dirpath, te->d_name); if (meow_font_probe_weight(tpath, de->d_name, we->d_name, te->d_name, label, sizeof label, cs, sizeof cs, &gc, wt, sizeof wt, nm, sizeof nm) == 0) continue; /* 缺组件/损坏 → 跳过该 tier 套 */ font_presets_add(de->d_name, we->d_name, te->d_name, label, cs, gc, wt, nm); } closedir(t); } closedir(w); } closedir(d); } /* M2.58:扫描结果落盘为 /MeowOS/config/fonts/index(含默认行 index0), * 作为 SDK meow_font_get_available_count()/enumerate() 的持久数据源;无卡时仅默认 1 行, * 由 meow_font_write_index 内部 mkdir-fail 静默放弃(此路径不刷屏)。 * M2.82+ 修补:挂载点目录在无卡时也存在(mkdir 骗过 ENOENT 检查)→ 开机早扫曾把 * 卡上 18 条 index 覆盖成 1 条默认。故:fonts 根目录根本没打开(opendir 失败)且 * 只剩默认项时,判定为「卡在但字库目录不可读/未就绪」→ 不写 index,保留旧索引。 */ if (!d && font_presets_n <= 1) { logmsg("fonts: scan root unavailable, keep existing index (%d presets in mem)", font_presets_n); return; } meow_font_write_index(card_root); } /* 设备默认卡根扫描(等价现状:无卡时 opendir 失败 → 仅默认字体)。 */ void meow_font_scan_presets(void) { meow_font_scan_presets_root("/usr/data/mnt/sd_0"); } /* MD3 tonal palette(565 参考值见 MD3_UI_SPEC §1;此处存 24-bit RGB,#hex) */ static const meow_ui_theme_t meow_ui_theme_dark = { 0x141824, 0x1E2436, 0x263038, 0x4A6A9A, 0xFFFFFF, 0x9AA4C8, 0x2A2E38, 0xE57373, 0x4A6A9A }; static const meow_ui_theme_t meow_ui_theme_light = { 0xE8EAF0, 0xD5D9E4, 0xCFD4E0, 0x2A5A8A, 0x1A1A2A, 0x4A4F5A, 0x9AA4C8, 0xB3261E, 0x2A5A8A }; /* MIUIx 族(§8.1.3 定案):暖色基面 + 小米橙 primary,供 launcher 主界面/设置页/通知中心/ * 电源菜单(s_ui_theme())在 ui.style=miuix 时使用(与 SDK meow_theme MIUIx 同色系)。 */ static const meow_ui_theme_t meow_ui_theme_miui_dark = { 0x181310, 0x2A231C, 0x2E2620, 0xFF8C4A, 0xEDE5DC, 0xAB9884, 0x4A3F35, 0xFFB4AB, 0xFF8C4A }; static const meow_ui_theme_t meow_ui_theme_miui_light = { 0xF7F3EE, 0xE9DFD0, 0xF4EDE3, 0xF57200, 0x251A10, 0x7D6A5A, 0xCBBFB2, 0xB3261E, 0xF57200 }; /* 获取当前本地时刻的「当天第几分钟」(tm_hour*60+tm_min);供时段判定用 */ static int s_now_min_of_day(void) { time_t t = time(NULL); struct tm tm; localtime_r(&t, &tm); return tm.tm_hour * 60 + tm.tm_min; } /* 深色时段判定(纯逻辑,便于单测):now_min ∈ [start, end) → 深色。 * - start==end 视为未配置,返回 -1(由调用方决定回退策略)。 * - startend:跨午夜,如 1140→420 = 19:00→次日07:00,now∈[1140,1440)∪[0,420) 为深色。 * - 边界:end 半开(恰好 end 分回到浅色),start 闭(恰好 start 分进入深色)。 */ static int theme_should_dark(int now_min, int start, int end) { if (start == end) return -1; /* 未配置 */ if (start < end) return (now_min >= start && now_min < end) ? 1 : 0; return (now_min >= start || now_min < end) ? 1 : 0; /* 跨午夜 */ } /* 当前主题(style×明暗 两套;接 ui.style + display.theme_mode + display.theme)。 * style(§8.1.1):ui.style 的 md3/miuix → 返回对应体系(MD3 tonal / MIUIx 暖色+橙)。 * manual(mode==THEME_MODE_MANUAL):直读 display.theme(dark/light),保持原有行为。 * auto(mode==THEME_MODE_AUTO):读深色时段两键 + 当前本地分钟;start==end 未配置 → * 回退 manual(读 display.theme,避免误判全天深色)。 * 读取失败一律回退 dark。 */ const meow_ui_theme_t *s_ui_theme(void) { int style = 0; /* ui.style:0=md3 / 1=miuix */ if (meow_settings_get_enum("ui.style", &style) != 0) style = 0; const meow_ui_theme_t *light = (style == 1) ? &meow_ui_theme_miui_light : &meow_ui_theme_light; const meow_ui_theme_t *dark = (style == 1) ? &meow_ui_theme_miui_dark : &meow_ui_theme_dark; int mode = THEME_MODE_MANUAL; if (meow_settings_get_enum("display.theme_mode", &mode) != 0) mode = THEME_MODE_MANUAL; int v = 0; /* display.theme(manual)当前索引 */ int have_manual = (meow_settings_get_enum("display.theme", &v) == 0); if (mode == THEME_MODE_AUTO) { int start = 0, end = 0; meow_settings_get_int("display.theme_dark_start", &start); meow_settings_get_int("display.theme_dark_end", &end); int d = theme_should_dark(s_now_min_of_day(), start, end); if (d < 0) { /* 未配置:回退 manual(display.theme) */ if (have_manual && v == 1) return light; return dark; } return d ? dark : light; } /* manual:照旧读 display.theme */ if (have_manual && v == 1) return light; return dark; }