740 lines
28 KiB
C
740 lines
28 KiB
C
/* 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 高频写,调用方在低频点(如音量渐变结束)才落盘一次。
|
||
*
|
||
* 空实现风格:本文件自包含(不依赖 meow_launcherd.c 的 logmsg),文件 I/O 失败仅打点,
|
||
* 绝不崩(内存继续用,符合 SYSTEM_SETTINGS.md §8「无卡/无板载用内存缓存」)。
|
||
*/
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <strings.h> /* strcasecmp */
|
||
#include <stdarg.h>
|
||
#include <fcntl.h>
|
||
#include <unistd.h>
|
||
#include <errno.h>
|
||
|
||
#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 */
|
||
|
||
/* ---- 默认值表:与 SYSTEM_SETTINGS.md §1 逐键一致(新增设置照附录扩展)---- */
|
||
static const meow_setting_def g_defs[] = {
|
||
/* 显示 */
|
||
{ "display.brightness", SET_INT, "101", 0, 255, 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.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/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,miui", "Material You,MIUI" },
|
||
/* 通知 */
|
||
{ "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 },
|
||
/* 日志 */
|
||
{ "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 公布 <host>.local) */
|
||
{ "system.hostname", SET_STR, "meowos", 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);
|
||
}
|
||
|
||
/* ---- 按类型把字符串值 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) 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;
|
||
}
|
||
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
|
||
if (!f) {
|
||
sset_log("no %s, defaults active", MEOW_SETTINGS_FILE);
|
||
g_inited = 1;
|
||
return MEOW_OK;
|
||
}
|
||
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;
|
||
}
|
||
|
||
/* ---- 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);
|
||
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)++;
|
||
}
|
||
}
|