/* meow_settings.c — MeowOS 系统设置后端(阶段 B2a) * * 实现与设计:SYSTEM_SETTINGS.md §1/§2/§3 + MEOWOS_API.md §2。 * - 统一存储:板载 /usr/data/meow/settings.conf(UBIFS,KB 级小文件,扁平 key=value)。 * 导入/导出(备份)走同一后端;默认备份路径 TF 卡 MeowOS/backups/settings/setting.bak。 * - 单点数据源:全部设置键存于本模块内存表(名称/类型/默认/取值域/当前值), * 实体 UI / Web 面板 / CLI 全走 meow_settings_* API,不留旁路。 * - clamp/校验集中 set(亮度 auto clamp >= display.brightness_min;枚举越界拒绝; * str 截断 MEOW_SET_MAX_LEN);set 成功后标记脏 + 向 listener 广播(key 字符串)。 * - 落盘:meow_settings_save()(写板载 settings.conf,保留 "# meow-settings 1" 头)。 * 为避免 UBIFS 高频写,调用方在低频点(如音量渐变结束)才落盘一次。 * * 空实现风格:本文件自包含(不依赖 launcher 主模块的 logmsg —— 现归 util.c),文件 I/O 失败仅打点, * 绝不崩(内存继续用,符合 SYSTEM_SETTINGS.md §8「无卡/无板载用内存缓存」)。 */ #include #include #include #include /* strcasecmp */ #include #include #include #include #include "meow_settings.h" /* 存储路径(板载 UBIFS 小文件)/ 默认备份路径(TF 卡)。 * 允许编译期覆盖(供宿主机单元测试指向临时文件;设备上默认不变)。 */ #ifndef MEOW_SETTINGS_FILE #define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf" #endif #ifndef MEOW_SETTINGS_BAK #define MEOW_SETTINGS_BAK "/usr/data/mnt/sd_0/MeowOS/backups/settings/setting.bak" #endif /* 表/监听器/临时缓冲上限 */ #define MEOW_SET_MAX_KEYS 64 #define MEOW_SET_MAX_LISTENERS 16 #define MEOW_SET_NAME_LEN 48 #define MEOW_SET_ENUM_LEN 96 #define MEOW_SET_LABELS_LEN 160 #define MEOW_SET_ENUM_MAX_OPT 8 /* 一个设置项:名称/类型/默认/当前值(统一字符串)+ clamp 域 + 枚举名/标签 */ typedef struct meow_setting { char key[MEOW_SET_NAME_LEN]; meow_set_type type; char def[MEOW_SET_MAX_LEN]; char cur[MEOW_SET_MAX_LEN]; int min, max; /* SET_INT 取值域(max<=0 表示仅保底、无上界) */ char enum_names[MEOW_SET_ENUM_LEN]; char enum_labels[MEOW_SET_LABELS_LEN]; int dirty; } meow_setting; static meow_setting g_sets[MEOW_SET_MAX_KEYS]; static int g_nsets = 0; static meow_settings_listener g_listeners[MEOW_SET_MAX_LISTENERS]; static int g_nlisteners = 0; static int g_inited = 0; static int g_broadcasting = 0; /* 防 listener 递归 set */ static int g_loaded_from_file = 0; /* M2.86:settings.conf 是否真正从盘读到(区分「默认值激活」 * 因 /usr/data 未就绪 fopen 失败 vs 文件已读但键缺失→默认) */ static int g_no_file_logged = 0; /* M2.86:本次「未读到」连续段是否已打过 no-file 日志(防重启重试刷屏) */ /* ---- 默认值表:与 SYSTEM_SETTINGS.md §1 逐键一致(新增设置照附录扩展)---- */ static const meow_setting_def g_defs[] = { /* 显示 */ { "display.brightness", SET_INT, "50", 0, 100, NULL, NULL }, { "display.brightness_min", SET_INT, "1", 0, 255, NULL, NULL }, { "display.screen_off_mode", SET_ENUM, "panel",0, 0, "panel,black", "面板下电,背光全黑" }, { "display.screen_off_timeout", SET_INT, "30", 0, 86400, NULL, NULL }, { "display.refresh_rate", SET_ENUM, "60", 0, 0, "60,30,15", "60Hz,30Hz,15Hz" }, { "display.refresh_always_max", SET_BOOL, "0", 0, 0, NULL, NULL }, { "display.theme", SET_ENUM, "dark", 0, 0, "dark,light", "深色,浅色" }, /* B2b 主题三方式:display.theme_mode 决定判定方式(manual=读 display.theme; * auto=按深色时段)。时段两键为分钟数 0-1439,start==end 视为未配置(回退 manual)。 * 注释:SYSTEM_SETTINGS.md §1 同步新增三行。 */ { "display.theme_mode", SET_ENUM, "manual", 0, 0, "manual,auto", "手动,时段自动" }, { "display.theme_dark_start", SET_INT, "0", 0, 1439, NULL, NULL }, { "display.theme_dark_end", SET_INT, "0", 0, 1439, NULL, NULL }, { "display.background", SET_STR, "black",0, 0, NULL, NULL }, /* M2.79 主界面背景图片叠加层强度(int 0-255,缺省 128):图片背景上叠 黑(深色)/白(浅色) * 半透明遮罩,保证控件/文字可读。仅图片背景(display.background=路径)时生效。 */ { "display.bg_overlay_alpha", SET_INT, "128", 0, 255, NULL, NULL }, /* M2.79 主界面布局卡片宫格/列表(card=0 默认 / list=1),设置页切换写 settings.conf。 */ { "ui.home_layout", SET_ENUM, "card", 0, 0, "card,list", "卡片宫格,列表" }, /* M2.5x 多字体:1=按字符类别逐字符路由(配合下方 6 类 font.class_*); * 0=单字体(只走 font.file,零额外内存/性能开销,等价现状)。 */ { "display.multi_font", SET_INT, "0", 0, 1, NULL, NULL }, /* 电源 */ { "power.longpress_menu_ms", SET_INT, "3000", 0, 0, NULL, NULL }, { "power.forceoff_ms", SET_INT, "8000", 0, 0, NULL, NULL }, { "power.defeat_hotkey", SET_STR, "power_voldown", 0,0, NULL, NULL }, /* 开机 */ { "boot.default", SET_ENUM, "meow", 0, 0, "meow,hiby", "MeowOS,HiByOS" }, { "boot.menu_enabled", SET_BOOL, "1", 0, 0, NULL, NULL }, { "boot.timeout_ms", SET_INT, "5000", 0, 0, NULL, NULL }, /* 输入 */ { "input.hotkey_seq", SET_STR, "volup,volup,volup", 0,0, NULL, NULL }, { "input.hotkey_window_ms", SET_INT, "2500", 0, 0, NULL, NULL }, { "input.touch_arm_delay_ms", SET_INT, "700", 0, 0, NULL, NULL }, /* M2.38:设置页滑动方向切换(0=标准:手指上滑→列表往下翻;1=反向) */ { "input.swipe_invert", SET_BOOL, "0", 0, 0, NULL, NULL }, /* 字体/主题 */ { "font.file", SET_STR, "MeowOS/fonts/default/regular/font.png", 0,0, NULL, NULL }, /* M2.5x 多字体:6 个字符类别各一字体(SET_STR,值=字体 family 相对路径,缺省空=跟随全局 font.file)。 * 仅 display.multi_font=1 时生效;同一 family 去重;上限 4 套不同字体(超出 WARN)。 */ { "font.class_cjk", SET_STR, "", 0,0, NULL, NULL }, { "font.class_cjk_sym", SET_STR, "", 0,0, NULL, NULL }, { "font.class_num", SET_STR, "", 0,0, NULL, NULL }, { "font.class_latin", SET_STR, "", 0,0, NULL, NULL }, { "font.class_latin_sym", SET_STR, "", 0,0, NULL, NULL }, { "font.class_other", SET_STR, "", 0,0, NULL, NULL }, /* M2.5x 字体档位分层 / 按需大字库:font.load_full(int 0/1 默认 0)=「加载大字库」开关, * 1=强制全系统用大字库 gbk≈20k 档(覆盖 app 未声明/覆盖默认 font.file); * 0=默认小字库 common≈4k,仅 app 声明 fonts:"full" 才大字库。见 §5.2/§5.7.8。 */ { "font.load_full", SET_INT, "0", 0, 1, NULL, NULL }, { "font.size.title", SET_INT, "28", 0, 0, NULL, NULL }, { "font.size.body", SET_INT, "20", 0, 0, NULL, NULL }, { "ui.style", SET_ENUM, "md3", 0, 0, "md3,miuix", "Material You,MIUIx" }, /* 通知 */ { "notify.center_enabled", SET_BOOL, "1", 0, 0, NULL, NULL }, { "notify.app_quota", SET_INT, "16", 0, 0, NULL, NULL }, /* M2a:通知配额(NOTIFICATION_SYSTEM.md §7,用户拍板 2026-08-29)。 * notify.categories = 每 App 最多申请的通知类型数(≤4) * notify.category_quota = 每类型活跃(未过期)通知上限(默认 4) * notify.media_quota = 每 App 媒体通知上限(默认 1;允许多媒体 App 同时) */ { "notify.categories", SET_INT, "4", 1, 4, NULL, NULL }, { "notify.category_quota", SET_INT, "4", 1, 16, NULL, NULL }, { "notify.media_quota", SET_INT, "1", 1, 1, NULL, NULL }, /* 通知中心「勿扰」面板内开关(SET_BOOL,默认 0=关):开=只显示 urgency>=2(HIGH/CRITICAL)。 * 面板内 toggle 读写均走 meow_settings_set_bool/get_bool,零旁路(通知中心 UI 方案 §12.2)。 */ { "notify.dnd", SET_BOOL, "0", 0, 0, NULL, NULL }, /* 日志 */ { "log.level", SET_ENUM, "info", 0, 0, "error,warn,info,debug", "错误,警告,信息,调试" }, { "log.to_card", SET_BOOL, "1", 0, 0, NULL, NULL }, /* 音频 */ { "audio.volume", SET_INT, "60", 0, 100, NULL, NULL }, { "audio.volume_max", SET_INT, "100", 0, 100, NULL, NULL }, { "audio.volume_step", SET_INT, "1", 1, 5, NULL, NULL }, { "audio.output", SET_STR, "hw:0,0", 0,0, NULL, NULL }, /* 网络(M2.39 设置页 WiFi/蓝牙开关;wifi 默认开=允许 ensure_wifi 自动连网, * 用户关=0 则 ensure_wifi 不再自动拉起 / 蓝牙默认关=少耗电) */ { "network.wifi_enabled", SET_BOOL, "1", 0, 0, NULL, NULL }, { "network.bluetooth_enabled", SET_BOOL, "0", 0, 0, NULL, NULL }, /* 系统(M2.54:主机名,bootmenu S89meow_svcd 开机读 system.hostname sethostname; * 缺省 meowos;RFC1035 首标签,供 meow_mdns 公布 .local) */ { "system.hostname", SET_STR, "meowos", 0, 0, NULL, NULL }, /* 锁屏 4 位 PIN(系统级安全;格式与面板 panel.pin 全等: * `:`,salt 16B /dev/urandom → 32 hex,绝不存明文。 * 空 = 未设锁屏 PIN(锁屏功能关闭)。与面板同源:panel.pin 已设而本键未设时,锁屏 * 接受 panel.pin 并在首次成功后并入本键(lck_pin_success,见 lock_screen.c)。 */ { "system.pin", SET_STR, "", 0, 0, NULL, NULL }, /* 息屏唤醒后锁屏(int 0/1,默认 1=开)。0=关(唤醒直进主屏;电源菜单「锁定」手动锁仍可用)。 */ { "system.pin_lock_on_wake", SET_INT, "1", 0, 1, NULL, NULL }, /* 错 PIN 锁定截止戳(int,epoch 秒;0=未锁定)。**持久化到 settings**(防 launcher 重启 * 清零——面板进程内存方案在重启后失效,此处落盘)。截止前锁屏页禁输入并倒计。 */ { "system.pin_lock_until", SET_INT, "0", 0, 0, NULL, NULL }, /* 连续错 PIN 计数(int 0-4;达 5 触发锁定并清零)。持久化防「关机重启重试」。 */ { "system.pin_fails", SET_INT, "0", 0, 0, NULL, NULL }, /* 面板访问级 PIN(meowpanel 写入,格式与本表 system.pin 全等)。launcher 注册它仅为: * ①锁屏迁移期读取(system.pin 未设时接受 panel.pin);②本表 save 重写全文件时**保留** * 该行(write_full_conf 只写已注册键,不注册会把面板已设的 panel.pin 抹掉)。launcher 不写它。 */ { "panel.pin", SET_STR, "", 0, 0, NULL, NULL }, }; /* ---- 日志(自包含,不依赖 launcher 的 logmsg;前缀 "settings:")---- */ static void sset_log(const char *fmt, ...) { char msg[256]; va_list ap; va_start(ap, fmt); vsnprintf(msg, sizeof msg, fmt, ap); va_end(ap); fprintf(stderr, "settings: %s\n", msg); } /* ---- 内部查找 ---- */ static meow_setting *find_set(const char *key) { if (!key) return NULL; for (int i = 0; i < g_nsets; i++) if (!strcmp(g_sets[i].key, key)) return &g_sets[i]; return NULL; } /* ---- 枚举名/标签的逗号串切分辅助 ---- */ static int enum_count_str(const char *names) { if (!names || !names[0]) return 0; int n = 1; for (const char *p = names; *p; p++) if (*p == ',') n++; return n; } /* 取第 idx 个名字(返回指向 names 内部,静态覆盖;用完即弃) */ static const char *enum_at_str(const char *names, int idx) { static char buf[MEOW_SET_ENUM_LEN * 2]; if (!names || idx < 0) return NULL; strncpy(buf, names, sizeof buf - 1); buf[sizeof buf - 1] = 0; char *p = buf; for (int i = 0; i < idx; i++) { while (*p && *p != ',') p++; if (*p == ',') p++; else return NULL; } while (*p == ' ' || *p == '\t') p++; /* 截到逗号/结尾 */ char *e = p; while (*e && *e != ',') e++; *e = 0; return p; } static int enum_index_of_name_str(const char *names, const char *name) { if (!names || !name) return -1; int n = enum_count_str(names); for (int i = 0; i < n; i++) { const char *s = enum_at_str(names, i); if (s && !strcmp(s, name)) return i; } return -1; } /* ---- clamp/校验集中:int 取值域(max<=0 → 仅保底)---- */ static int clamp_int(meow_setting *s, int v) { if (s->max > 0) { if (v < s->min) v = s->min; if (v > s->max) v = s->max; } else { if (v < s->min) v = s->min; } return v; } /* display.brightness 额外 clamp >= display.brightness_min(防黑屏,SYSTEM_SETTINGS §3) * 且 M2.39 上限 100(max_brightness=101,但 101 会闪屏,用户确认上限 100)。 */ static int clamp_brightness(int v) { int mn = 1; /* def 默认(brightness_min 未 init/空时用默认 1=极暗但非全黑,屏仍可见) */ meow_setting *bm = find_set("display.brightness_min"); if (bm && bm->cur[0]) mn = atoi(bm->cur); if (v < mn) v = mn; if (v > 100) v = 100; return v; } /* M2.53:返回当前用户亮度设定(display.brightness)经 clamp_brightness 后的值 * (下限 brightness_min 默认 1、上限 100)。MeowOS 主屏接管时用它压回用户亮度, * 覆盖 HibyOS 播放器写入的背光下限 16/25。settings 未 init/键缺失回退默认 101 再 clamp * (→100,不闪屏)。纯函数,宿主机可直接测(MEOW_SETTINGS_FILE 可编译期覆盖)。 */ int meow_settings_brightness_clamped(void) { meow_setting *b = find_set("display.brightness"); int v = 101; /* 默认表 display.brightness 默认 */ if (b && b->cur[0]) v = atoi(b->cur); return clamp_brightness(v); } /* M2.86:settings.conf 是否真正从盘上读到过(最近一次 meow_settings_init 是否成功打开了文件)。 * 0=S70 早启动 /usr/data 未就绪 fopen 失败 → 当前全为默认值(display.brightness 101→将被 clamp 到 100), * 此时调用方(尤其 apply_user_brightness)不应把默认值当作用户设定写入,应挂起待设置就绪。 * 1=文件已读到:键缺失则走各自默认(属「已知」态),用户设定可信。 */ int meow_settings_loaded(void) { return g_loaded_from_file; } /* 存储文件路径(= 编译期 MEOW_SETTINGS_FILE,默认 /usr/data/meow/settings.conf)。 * 供跨进程变更轮询(launcher App 前台会话 stat mtime,见 app_launcher.c)等场景取权威路径, * 避免调用方硬编码与编译期覆盖分叉。 */ const char *meow_settings_file_path(void) { return MEOW_SETTINGS_FILE; } /* ---- 按类型把字符串值 apply 到 cur(读文件/导入时用;含 clamp/校验)---- */ static void apply_value(meow_setting *s, const char *v) { if (!s || !v) return; switch (s->type) { case SET_INT: { int iv = atoi(v); iv = clamp_int(s, iv); if (!strcmp(s->key, "display.brightness")) iv = clamp_brightness(iv); snprintf(s->cur, sizeof s->cur, "%d", iv); break; } case SET_BOOL: { int b = 0; char p[16], *q = p; strncpy(p, v, sizeof p - 1); p[sizeof p - 1] = 0; while (*q == ' ' || *q == '\t') q++; long n = strtol(q, NULL, 10); if (!strcasecmp(q, "true") || !strcasecmp(q, "yes") || !strcasecmp(q, "on")) b = 1; else if (q[0] && n != 0) b = 1; /* 数值非 0(含旧风格 "2")视为 true;向后兼容 */ snprintf(s->cur, sizeof s->cur, "%d", b); break; } case SET_ENUM: { int idx = enum_index_of_name_str(s->enum_names, v); if (idx < 0) { /* §8.1.1 注:旧版 ui.style 值域为 "md3,miui",本版对齐为 "md3,miuix"——存量 * settings.conf / 备份里的 "miui" 应兼容映射为 "miuix"(而非 atoi 落到 md3=0)。 */ if (!strcmp(s->key, "ui.style") && strcasecmp(v, "miui") == 0) idx = enum_index_of_name_str(s->enum_names, "miuix"); if (idx < 0) idx = atoi(v); /* 向后兼容:数值索引 */ } if (idx >= 0 && idx < enum_count_str(s->enum_names)) { const char *name = enum_at_str(s->enum_names, idx); if (name) strncpy(s->cur, name, sizeof s->cur - 1); } /* 未知值忽略(保留默认) */ break; } case SET_STR: strncpy(s->cur, v, sizeof s->cur - 1); s->cur[sizeof s->cur - 1] = 0; break; } s->dirty = 0; } /* ---- 广播(防 listener 递归 set)---- */ static void notify_key(const char *key) { if (g_broadcasting) return; g_broadcasting = 1; for (int i = 0; i < g_nlisteners; i++) if (g_listeners[i]) g_listeners[i](key); g_broadcasting = 0; } /* import 内部:apply + 脏 + 广播(定义在下方 import 之前声明) */ static void apply_value_nosave(meow_setting *s, const char *v, int *applied); /* ---- 注册 ---- */ int meow_settings_register(const meow_setting_def *def) { if (!def || !def->key) return MEOW_ERR_INVAL; if (g_nsets >= MEOW_SET_MAX_KEYS) return MEOW_ERR_NOMEM; if (find_set(def->key)) return MEOW_ERR_EXISTS; meow_setting *s = &g_sets[g_nsets++]; memset(s, 0, sizeof *s); strncpy(s->key, def->key, sizeof s->key - 1); s->type = def->type; strncpy(s->def, def->def ? def->def : "", sizeof s->def - 1); strncpy(s->cur, s->def, sizeof s->cur - 1); s->min = def->min; s->max = def->max; if (def->enum_names) strncpy(s->enum_names, def->enum_names, sizeof s->enum_names - 1); if (def->enum_labels) strncpy(s->enum_labels, def->enum_labels, sizeof s->enum_labels - 1); s->dirty = 0; return MEOW_OK; } /* ---- 初始化(幂等;重载=重置默认 + 重读 settings.conf)---- */ int meow_settings_init(void) { if (g_nsets == 0) { for (unsigned i = 0; i < sizeof g_defs / sizeof g_defs[0]; i++) { int rc = meow_settings_register(&g_defs[i]); /* 表满/重复为内部异常;防御性继续(注册失败仅该键缺失) */ (void)rc; } } /* 重置全部到默认,再读文件(与旧 settings_load 语义一致:默认 + 覆盖) */ for (int i = 0; i < g_nsets; i++) { strncpy(g_sets[i].cur, g_sets[i].def, sizeof g_sets[i].cur - 1); g_sets[i].dirty = 0; } /* M2.86:每次重载先把「已读到盘上」置 0;成功打开才置 1。 * 区分「settings.conf 被读到(键缺失→默认)」与「完全没读到(/usr/data 未就绪 → 纯默认)」,后者 * 调用方(apply_user_brightness)不得把默认值当作用户设定盲目写盘(尤其 display.brightness 101→clamp * 100 会误把开机亮度压到满亮)。 */ g_loaded_from_file = 0; FILE *f = fopen(MEOW_SETTINGS_FILE, "r"); if (!f) { /* M2.86:S70 早启动 /usr/data(UBIFS) 未就绪时 fopen 失败是常态,且 run_menu 每 2s 重试 * settings_load —— 只在本次「未读到」连续段首次打日志,避免每 2s 刷一行「no file」。 */ if (!g_no_file_logged) { sset_log("no %s, defaults active", MEOW_SETTINGS_FILE); g_no_file_logged = 1; } g_inited = 1; return MEOW_OK; } g_loaded_from_file = 1; /* 文件已打开待读:即便 0 行也视为「已读到」→ 键缺失走默认而非「未知」 */ g_no_file_logged = 0; /* 段复位:下次失败(若发生)重新计一句话 */ char buf[768]; while (fgets(buf, sizeof buf, f)) { char *eq = strchr(buf, '='); if (!eq) continue; *eq = 0; /* 左去空 */ char *k = buf; while (*k == ' ' || *k == '\t') k++; int kl = (int)strlen(k); while (kl > 0 && (k[kl-1] == ' ' || k[kl-1] == '\t')) k[--kl] = 0; if (!k[0] || k[0] == '#') continue; /* 空行/注释 */ /* 右值去空 */ char *v = eq + 1; while (*v == ' ' || *v == '\t') v++; int vl = (int)strlen(v); while (vl > 0 && (v[vl-1] == '\n' || v[vl-1] == '\r')) v[--vl] = 0; while (vl > 0 && (v[vl-1] == ' ' || v[vl-1] == '\t')) v[--vl] = 0; meow_setting *s = find_set(k); if (s) apply_value(s, v); /* 未知键忽略(SYSTEM_SETTINGS §3 存储格式) */ } fclose(f); /* 读取后统一补一次亮度下限 clamp(brightness_min 可能在 brightness 之后读入) */ meow_setting *b = find_set("display.brightness"); if (b) { int iv = atoi(b->cur); int cv = clamp_brightness(iv); snprintf(b->cur, sizeof b->cur, "%d", cv); } g_inited = 1; /* M2.39:迁移旧持久亮度下限 25 → 1(SYSTEM_SETTINGS §1 下限默认已改 1,但设备 * settings.conf 里还残留旧版写入的 display.brightness_min=25,覆盖默认表,导致亮度 * 滑条被 clamp 在 25 调不下去;且 SMENU 设置页并不暴露 brightness_min 这一项,故 25 * 只能是旧默认残留→强制视为待迁移,改写为 1 并落盘,让用户能调到「极暗但可见」的 1)。 * 此处只针对恰为旧默认 25 的值;用户其它自定义值(非 25)不动。 */ meow_setting *bmin = find_set("display.brightness_min"); if (bmin && !strcmp(bmin->cur, "25")) { snprintf(bmin->cur, sizeof bmin->cur, "%d", 1); bmin->dirty = 1; sset_log("brightness_min stale default 25 -> 1 (migrated)"); meow_settings_save(); } return MEOW_OK; } /* ---- 生命周期 ---- */ int meow_settings_shutdown(void) { if (g_inited) meow_settings_save(); return MEOW_OK; } void meow_settings_dump(const char *log_prefix) { const char *lp = log_prefix ? log_prefix : ""; for (int i = 0; i < g_nsets; i++) sset_log("%s%s = %s", lp, g_sets[i].key, g_sets[i].cur); } /* ---- get ---- */ int meow_settings_get_int(const char *key, int *out) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (!g_inited) return MEOW_ERR_NOTINIT; if (s->type != SET_INT) return MEOW_ERR_INVAL; if (!out) return MEOW_ERR_INVAL; *out = atoi(s->cur); return MEOW_OK; } int meow_settings_get_str(const char *key, char *buf, int buf_len) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (!g_inited) return MEOW_ERR_NOTINIT; if (s->type != SET_STR) return MEOW_ERR_INVAL; if (!buf || buf_len <= 0) return MEOW_ERR_INVAL; strncpy(buf, s->cur, buf_len); buf[buf_len - 1] = 0; return MEOW_OK; } int meow_settings_get_enum(const char *key, int *out) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (!g_inited) return MEOW_ERR_NOTINIT; if (s->type != SET_ENUM) return MEOW_ERR_INVAL; if (!out) return MEOW_ERR_INVAL; *out = enum_index_of_name_str(s->enum_names, s->cur); if (*out < 0) *out = 0; /* 非法当前值兜底:首项 */ return MEOW_OK; } int meow_settings_get_bool(const char *key, int *out) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (!g_inited) return MEOW_ERR_NOTINIT; if (s->type != SET_BOOL) return MEOW_ERR_INVAL; if (!out) return MEOW_ERR_INVAL; *out = (s->cur[0] == '1'); return MEOW_OK; } /* ---- 标注脏 + 广播的公共尾部 ---- */ static void mark_changed(meow_setting *s, const char *newval) { if (strcmp(newval, s->cur) == 0) return; /* 未变:不广播 */ strncpy(s->cur, newval, sizeof s->cur - 1); s->cur[sizeof s->cur - 1] = 0; s->dirty = 1; notify_key(s->key); } /* ---- set ---- */ int meow_settings_set_int(const char *key, int v) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (!g_inited) return MEOW_ERR_NOTINIT; if (s->type != SET_INT) return MEOW_ERR_INVAL; v = clamp_int(s, v); if (!strcmp(s->key, "display.brightness")) v = clamp_brightness(v); char tmp[24]; snprintf(tmp, sizeof tmp, "%d", v); mark_changed(s, tmp); return MEOW_OK; } int meow_settings_set_str(const char *key, const char *v) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (!g_inited) return MEOW_ERR_NOTINIT; if (s->type != SET_STR) return MEOW_ERR_INVAL; char tmp[MEOW_SET_MAX_LEN]; strncpy(tmp, v ? v : "", sizeof tmp - 1); tmp[sizeof tmp - 1] = 0; mark_changed(s, tmp); return MEOW_OK; } int meow_settings_set_enum(const char *key, int idx) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (!g_inited) return MEOW_ERR_NOTINIT; if (s->type != SET_ENUM) return MEOW_ERR_INVAL; int n = enum_count_str(s->enum_names); if (idx < 0 || idx >= n) return MEOW_ERR_INVAL; /* 越界拒绝 */ const char *name = enum_at_str(s->enum_names, idx); if (!name) return MEOW_ERR_INVAL; mark_changed(s, name); return MEOW_OK; } int meow_settings_set_bool(const char *key, int v) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (!g_inited) return MEOW_ERR_NOTINIT; if (s->type != SET_BOOL) return MEOW_ERR_INVAL; char tmp[8]; snprintf(tmp, sizeof tmp, "%d", v ? 1 : 0); mark_changed(s, tmp); return MEOW_OK; } /* ---- 开关型(M2.5x)---- * 开关 = SET_BOOL,或 SET_INT 且取值域恰为 {0,1}(如 display.multi_font / font.load_full, * 见 SYSTEM_SETTINGS §1「int 0/1」)。实体设置页 SC_SWITCH 与字体逻辑用通用读写, * 避免对 SET_INT 开关误用 get_bool/set_bool(类型不符必失败 → 点不动/恒 OFF/6 类行恒隐藏)。 */ static int is_switch_like(meow_setting *s) { if (!s) return 0; if (s->type == SET_BOOL) return 1; if (s->type == SET_INT && s->min == 0 && s->max == 1) return 1; return 0; } int meow_settings_get_switch(const char *key, int *out) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (!g_inited) return MEOW_ERR_NOTINIT; if (!out) return MEOW_ERR_INVAL; if (!is_switch_like(s)) return MEOW_ERR_INVAL; if (s->type == SET_BOOL) *out = (s->cur[0] == '1'); else *out = (atoi(s->cur) != 0); return MEOW_OK; } int meow_settings_set_switch(const char *key, int v) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (!g_inited) return MEOW_ERR_NOTINIT; if (!is_switch_like(s)) return MEOW_ERR_INVAL; if (s->type == SET_BOOL) return meow_settings_set_bool(key, v); return meow_settings_set_int(key, v ? 1 : 0); /* SET_INT 0/1:写 0/1(clamp 到 {0,1}) */ } /* ---- listener ---- */ int meow_settings_add_listener(meow_settings_listener cb) { if (!cb) return MEOW_ERR_INVAL; for (int i = 0; i < g_nlisteners; i++) if (g_listeners[i] == cb) return MEOW_ERR_EXISTS; if (g_nlisteners >= MEOW_SET_MAX_LISTENERS) return MEOW_ERR_NOMEM; g_listeners[g_nlisteners++] = cb; return MEOW_OK; } int meow_settings_remove_listener(meow_settings_listener cb) { for (int i = 0; i < g_nlisteners; i++) { if (g_listeners[i] == cb) { memmove(&g_listeners[i], &g_listeners[i+1], (g_nlisteners - i - 1) * sizeof *g_listeners); g_nlisteners--; return MEOW_OK; } } return MEOW_ERR_NOENT; } /* ---- enum 辅助 ---- */ int meow_settings_enum_label(const char *key, int idx, char *buf, int buf_len) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (s->type != SET_ENUM) return MEOW_ERR_INVAL; if (!buf || buf_len <= 0) return MEOW_ERR_INVAL; if (idx < 0 || idx >= enum_count_str(s->enum_names)) return MEOW_ERR_INVAL; const char *lab = enum_at_str(s->enum_labels, idx); if (!lab || !lab[0]) lab = enum_at_str(s->enum_names, idx); /* 无标签回退名字 */ if (!lab) return MEOW_ERR_INVAL; strncpy(buf, lab, buf_len); buf[buf_len - 1] = 0; return MEOW_OK; } int meow_settings_enum_count(const char *key) { meow_setting *s = find_set(key); if (!s) return -MEOW_ERR_NOENT; if (s->type != SET_ENUM) return -MEOW_ERR_INVAL; return enum_count_str(s->enum_names); } int meow_settings_enum_index_of_name(const char *key, const char *name) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (s->type != SET_ENUM) return MEOW_ERR_INVAL; return enum_index_of_name_str(s->enum_names, name); } void meow_settings_enum_names(const char *key, const char **out_names, int *n) { static char scratch[MEOW_SET_ENUM_LEN]; static const char *ptrs[MEOW_SET_ENUM_MAX_OPT]; if (n) *n = 0; meow_setting *s = find_set(key); if (!s || s->type != SET_ENUM || !out_names) return; strncpy(scratch, s->enum_names, sizeof scratch - 1); scratch[sizeof scratch - 1] = 0; char *p = scratch; int k = 0; while (*p && k < MEOW_SET_ENUM_MAX_OPT) { while (*p == ' ' || *p == '\t') p++; if (!*p) break; ptrs[k++] = p; while (*p && *p != ',') p++; if (*p == ',') { *p = 0; p++; } } for (int i = 0; i < k; i++) out_names[i] = ptrs[i]; if (n) *n = k; } /* ---- list ---- */ int meow_settings_list(int (*cb)(const char *key, meow_set_type t, void *ctx), void *ctx) { if (!cb) return MEOW_ERR_INVAL; if (!g_inited) return MEOW_ERR_NOTINIT; for (int i = 0; i < g_nsets; i++) cb(g_sets[i].key, g_sets[i].type, ctx); return MEOW_OK; } /* ---- 默认值 / 复位 ---- */ int meow_settings_get_default(const char *key, char *buf, int len) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (!buf || len <= 0) return MEOW_ERR_INVAL; strncpy(buf, s->def, len); buf[len - 1] = 0; return MEOW_OK; } int meow_settings_reset(const char *key) { meow_setting *s = find_set(key); if (!s) return MEOW_ERR_NOENT; if (strcmp(s->cur, s->def) != 0) { strncpy(s->cur, s->def, sizeof s->cur - 1); s->cur[sizeof s->cur - 1] = 0; s->dirty = 1; notify_key(s->key); } return MEOW_OK; } int meow_settings_reset_all(void) { for (int i = 0; i < g_nsets; i++) { meow_setting *s = &g_sets[i]; if (strcmp(s->cur, s->def) != 0) { strncpy(s->cur, s->def, sizeof s->cur - 1); s->cur[sizeof s->cur - 1] = 0; s->dirty = 1; notify_key(s->key); } } return MEOW_OK; } /* ---- 落盘(写板载 settings.conf;保留 "# meow-settings 1" 头)---- */ static void write_full_conf(int fd) { char buf[576]; int l = snprintf(buf, sizeof buf, "# meow-settings 1\n"); if (l > 0) write(fd, buf, l); for (int i = 0; i < g_nsets; i++) { meow_setting *s = &g_sets[i]; const char *val = s->cur; char num[24]; /* 非字符串类型把 cur 规范为可读形式(enum=名字,bool/int=数字) */ if (s->type == SET_INT) { snprintf(num, sizeof num, "%d", atoi(s->cur)); val = num; } else if (s->type == SET_BOOL) { snprintf(num, sizeof num, "%d", s->cur[0] == '1'); val = num; } else if (s->type == SET_ENUM) { val = s->cur; } l = snprintf(buf, sizeof buf, "%s = %s\n", s->key, val); if (l > 0) write(fd, buf, l); } } int meow_settings_save(void) { if (!g_inited) return MEOW_ERR_NOTINIT; if (system("mkdir -p /usr/data/meow 2>/dev/null") != 0) { /* 目录已存在等返回非 0 也属正常,不视为失败 */ } int fd = open(MEOW_SETTINGS_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { sset_log("save: open %s fail: %s", MEOW_SETTINGS_FILE, strerror(errno)); return MEOW_ERR_IO; } write_full_conf(fd); close(fd); /* boot.default:除写 settings.conf 外,还要同步到启动菜单的权威文件 /data/meow_boot_default * (bootmenu 开机读它定默认项;与 bootmenu set_boot_default_meow() 写 "0" / launcher * set_boot_default_hiby() 写 "1" 同文件、同语义:meow=0(默认 MeowOS), hiby=1(默认 HiByOS))。 * 幂等:值未变也 O_TRUNC 重写,无害。init 读入不触发 save,故不会反向覆盖权威文件。 */ meow_setting *bdef = find_set("boot.default"); if (bdef) { int v = (!strcmp(bdef->cur, "hiby")) ? 1 : 0; /* meow(默认) -> 0, hiby -> 1 */ int bf = open("/data/meow_boot_default", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (bf >= 0) { char bb[8]; int n = snprintf(bb, sizeof bb, "%d\n", v); write(bf, bb, n); close(bf); sset_log("boot.default=%s -> /data/meow_boot_default=%d", bdef->cur, v); } else { sset_log("boot.default sync: open /data/meow_boot_default fail: %s", strerror(errno)); } } for (int i = 0; i < g_nsets; i++) g_sets[i].dirty = 0; return MEOW_OK; } /* ---- export:写一份 .conf 到 path(默认 TF 卡备份)---- */ int meow_settings_export(const char *path) { const char *p = path ? path : MEOW_SETTINGS_BAK; /* 自动建目录(取 p 的 dirname),失败静默(导出尽力而为) */ { char dir[320]; char cmd[344]; snprintf(dir, sizeof dir, "%s", p); char *slash = strrchr(dir, '/'); if (slash) { if (slash == dir) slash[1] = 0; else *slash = 0; snprintf(cmd, sizeof cmd, "mkdir -p '%s' 2>/dev/null", dir); system(cmd); } } int fd = open(p, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { sset_log("export: open %s fail: %s", p, strerror(errno)); return MEOW_ERR_IO; } write_full_conf(fd); close(fd); sset_log("exported -> %s", p); return MEOW_OK; } /* ---- import:从 .conf 读入覆盖;逐键校验,损坏键不中断 ---- */ int meow_settings_import(const char *path) { if (!g_inited) return MEOW_ERR_NOTINIT; const char *p = path ? path : MEOW_SETTINGS_BAK; FILE *f = fopen(p, "r"); if (!f) { sset_log("import: open %s fail: %s", p, strerror(errno)); return MEOW_ERR_NOENT; } int bad = 0, applied = 0; char buf[768]; while (fgets(buf, sizeof buf, f)) { char *eq = strchr(buf, '='); if (!eq) continue; *eq = 0; char *k = buf; while (*k == ' ' || *k == '\t') k++; int kl = (int)strlen(k); while (kl > 0 && (k[kl-1] == ' ' || k[kl-1] == '\t')) k[--kl] = 0; if (!k[0] || k[0] == '#') continue; char *v = eq + 1; while (*v == ' ' || *v == '\t') v++; int vl = (int)strlen(v); while (vl > 0 && (v[vl-1] == '\n' || v[vl-1] == '\r')) v[--vl] = 0; while (vl > 0 && (v[vl-1] == ' ' || v[vl-1] == '\t')) v[--vl] = 0; meow_setting *s = find_set(k); if (!s) { bad++; continue; } /* 未知键:计损坏,不中断 */ /* 逐键校验:走 set 的 clamp/校验(apply_value 已 clamp);值合法才算成功 */ apply_value_nosave(s, v, &applied); } fclose(f); meow_settings_save(); /* 导入后落盘(应用覆盖) */ sset_log("imported %s: applied=%d invalid=%d", p, applied, bad); return applied > 0 ? MEOW_OK : MEOW_ERR_INVAL; } /* ---- import 内部:apply + 脏 + 广播(不重置整表)---- */ static void apply_value_nosave(meow_setting *s, const char *v, int *applied) { if (!s || !v) return; char before[MEOW_SET_MAX_LEN]; strncpy(before, s->cur, sizeof before - 1); before[sizeof before - 1] = 0; apply_value(s, v); if (strcmp(before, s->cur) != 0) { s->dirty = 1; notify_key(s->key); if (applied) (*applied)++; } }