Files

860 lines
36 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 <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 */
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 },
/* P1 蓝牙 P1:输出目标偏好(0=DAC/本地,1=蓝牙 OUT_BT)。audiod 侧 target 切换在 P2
* 实装;P1 仅作为「输出目标」行/摘要的持久化键(蓝牙子页写,DAC 侧不受影响)。 */
{ "audio.output_bt", SET_BOOL, "0", 0, 0, NULL, NULL },
/* P1 蓝牙:codec 偏好(auto/SBC/AAC/aptX/LDAC;P2 接 alsa.conf pcm.bt/bluealsa 生效)。
* 当前实际协商 codec 由 a2dp 状态实时读,本键只是页面「编解码器」选项的持久化。 */
{ "bt.codec", SET_STR, "auto", 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 },
/* 锁屏 4 位 PIN(系统级安全;格式与面板 panel.pin 全等:
* `<salt_hex>:<sha256_hex(salt||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)++;
}
}