diff --git a/meow_ui_img.c b/meow_ui_img.c index 0e6b2e8..c2bd08b 100644 --- a/meow_ui_img.c +++ b/meow_ui_img.c @@ -59,19 +59,23 @@ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover) unsigned char *out = (unsigned char *)malloc((size_t)dw * dh * 4); if (!out) return NULL; float s = 1.0f; - float ox = 0.0f, oy = 0.0f; if (cover) { + /* 真正铺满(cover):每目标像素对应 s 个源像素,s = min(W/dw, H/dh)——缩放后图像 + * 宽/高两维都不小于目标(可能外溢),居中裁方。居中裁剪偏移(源像素)= (W-dw*s)/2 + * 恒 ≥ 0。曾误作 `s = max(sx,sy)` 且采样 `(dx+ox)/s`(除以缩放比):把整块输出映射到 + * 源左上角 ~dw/s 像素的透明角——图标(透明角 logo)整块透明、不绘制,即 + * 「icon: ... OK 但渲染不出(只显首字母占位)」根因。正确:源索引 = dx*s + 裁剪偏移 + * (与下方非 cover 分支 `dx*im->w/dw` 同一乘向)。 */ float sx = (float)im->w / dw, sy = (float)im->h / dh; - s = sx > sy ? sx : sy; /* 铺满 → 可能外溢,居中裁 */ - float sw = im->w / s, sh = im->h / s; /* 目标显示寸(sw×sh) */ - ox = (float)(dw - sw) * 0.5f; - oy = (float)(dh - sh) * 0.5f; - if (ox < 0) ox = 0; if (oy < 0) oy = 0; + s = sx < sy ? sx : sy; /* 源像素/目标像素(铺满 → 取小) */ + float oxs = ((float)im->w - (float)dw * s) * 0.5f; /* 居中裁剪起点(源像素,≥0) */ + float oys = ((float)im->h - (float)dh * s) * 0.5f; + if (oxs < 0) oxs = 0; if (oys < 0) oys = 0; for (int dy = 0; dy < dh; dy++) { - int syi = (int)((dy + oy) / s); + int syi = (int)((float)dy * s + oys); if (syi < 0) syi = 0; if (syi >= im->h) syi = im->h - 1; for (int dx = 0; dx < dw; dx++) { - int sxi = (int)((dx + ox) / s); + int sxi = (int)((float)dx * s + oxs); if (sxi < 0) sxi = 0; if (sxi >= im->w) sxi = im->w - 1; memcpy(out + ((size_t)dy * dw + dx) * 4, im->rgba + ((size_t)syi * im->w + sxi) * 4, 4); diff --git a/meow_ui_input.c b/meow_ui_input.c index a931b65..93e5a51 100644 --- a/meow_ui_input.c +++ b/meow_ui_input.c @@ -6,12 +6,20 @@ * meow_ui_run / IME 循环弹出分发。应用不再自读 event*。 * * 触摸:BTN_TOUCH 按下记录原点,抬起时位移 < 阈值 → TAP,否则按主导轴 → SWIPE。 - * 物理键:EV_KEY 值 1=按下 / 0=抬起(重复值 2 忽略)。M2.24 全面按键接管:电源/音量/播放/ - * 上下曲全部由系统(watchdog/launcher)消费,SDK 吞噬不发给 APP(应用只收 tap/swipe/tick)。 + * 物理键:EV_KEY 值 1=按下 / 0=抬起(重复值 2 忽略)。M2.55 起系统按键语义在 SDK 输入层消化: + * 电源 116 → 维持现状(息屏/亮屏/长按菜单由内核/sys_server/watchdog),SDK 不动、不下发 APP; + * 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,volume_step/volume_max + * 约束,与 launcher 设置页/watchdog 同源); + * 播放/暂停 164、上一曲 165/19、下一曲 163 → 转发到当前媒体会话(App 是媒体源→本地回调, + * 否则经 IPC 发 launcher 转发)。 + * 「App 收到的是语义事件」:上述音量/媒体键在 SDK 消化后**仍下发 MEOW_EV_KEY**(on_key),但 + * App 不得把它们映射成自定义行为(APPS.md §3.4.1 语义不变)。 */ #include "meow_ui.h" #include "meow_ui_internal.h" #include "meow_ui_sys.h" +#include "meow_ui_vol.h" +#include "meow_ui_media.h" #include #include @@ -81,6 +89,45 @@ static uint64_t now_ms(void) return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u; } +/* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;设备默认同 meow_ui_sys.c)。 */ +static const char *meow_settings_path(void) +{ + const char *e = getenv("MEOW_SETTINGS_FILE"); + return (e && e[0]) ? e : "/usr/data/meow/settings.conf"; +} + +/* ---- 系统按键语义(M2.55:SDK 输入层消化,App 前台也生效)---- + * 物理键是系统资源(APPS.md §3.4.1),语义全局统一。本函数在输入线程执行系统动作: + * 电源 116 → 息屏/亮屏/长按菜单(内核/sys_server/watchdog),SDK 不动(维持现状); + * 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,与 launcher + * 设置页/watchdog 同源:volume_step/volume_max 约束),仅按下边沿(1)一步; + * 播放/上次曲 164/165/19/163 → 转发到当前媒体会话(App 是媒体源→本地回调;否则 IPC→launcher)。 + * 仅处理按下边沿(val==1)触发系统动作;重复(2)/抬起(0)不重复执行。on_key 的下发由输入线程 + * 在调用本函数后另行 push(音量/媒体键仍通知 App,App 不得映射)。 */ +static void meow_input_handle_sys_key(int code, int val) +{ + switch (code) { + case MEOW_KEY_POWER: /* 电源:维持现状,SDK 不动 */ + break; + case MEOW_KEY_VOLUMEDOWN: + case MEOW_KEY_VOLUMEUP: + if (val == 1) { + int dir = (code == MEOW_KEY_VOLUMEUP) ? +1 : -1; + int nv = meow_ui_vol_step(meow_settings_path(), dir); /* settings 语义+持久化 */ + meow_ui_vol_dac_write(nv); /* 写真实 CS43198 DAC */ + } + break; + case MEOW_KEY_PLAYPAUSE: + case MEOW_KEY_PREVIOUS: + case MEOW_KEY_NEXT: + case 19: /* KEY_PREVIOUSSONG 旧码 */ + if (val == 1) meow_ui_media_sys_key(code); + break; + default: + break; + } +} + void meow_input_drain(void) { pthread_mutex_lock(&g_qm); @@ -335,14 +382,25 @@ static void *input_thread(void *arg) if (code == BTN_TOUCH) { if (val && !t_down) touch_press(t_cx, t_cy); else if (!val && t_down) touch_release(); - } else if (code == MEOW_KEY_POWER || code == MEOW_KEY_VOLUMEDOWN || - code == MEOW_KEY_VOLUMEUP || code == MEOW_KEY_PLAYPAUSE || - code == MEOW_KEY_PREVIOUS || code == MEOW_KEY_NEXT || code == 19) { - /* M2.22(A)/M2.24 纪律:物理键(电源 116 / 音量 114,115 / 播放 164 / - * 上一曲 165(MEOW_KEY_PREVIOUS)/19(旧码) / 下一曲 163)全部是系统语义 —— watchdog/launcher 统一消费, - * 绝不下发 APP。应用不碰物理键、不把任何物理键映射成确认/exit/返回/翻页 - * (KEY_CONTROL_DESIGN §1.3 / APPS.md §3.4.1);APP 只接收 tap/swipe/tick。 - * 忽略该键,不发 MEOW_EV_KEY。 */ + } else if (code == MEOW_KEY_POWER) { + /* M2.55(接 M2.24 纪律):电源=息屏/亮屏/长按菜单,由内核/sys_server/ + * watchdog 统一处理,SDK 不动、也不下发 APP。维持现状。 */ + } else if (code == MEOW_KEY_VOLUMEDOWN || code == MEOW_KEY_VOLUMEUP) { + /* 音量键=系统音量(M2.55:SDK 输入层消化,App 前台也生效)。 + * 语义动作在 meow_input_handle_sys_key(仅按下边沿一步);随后仍下发 + * MEOW_EV_KEY(App on_key 收到语义事件,但不得映射成自定义行为)。 */ + if (val == 1 || val == 0) { + meow_input_handle_sys_key(code, val); + push_ev(MEOW_EV_KEY, code, (val == 1) ? 1 : 0); + } + } else if (code == MEOW_KEY_PLAYPAUSE || code == MEOW_KEY_PREVIOUS || + code == MEOW_KEY_NEXT || code == 19) { + /* 媒体键=转发到当前媒体会话(M2.55:SDK 输入层消化)。语义动作在 + * meow_input_handle_sys_key;随后仍下发 MEOW_EV_KEY(App 不得映射)。 */ + if (val == 1 || val == 0) { + meow_input_handle_sys_key(code, val); + push_ev(MEOW_EV_KEY, code, (val == 1) ? 1 : 0); + } } else if (val == 1) { /* 按下边沿(忽略重复=2)——仅未知键(本设备无) */ push_ev(MEOW_EV_KEY, code, 1); } else if (val == 0) { /* 抬起 */ diff --git a/meow_ui_media.c b/meow_ui_media.c index 6063642..3632302 100644 --- a/meow_ui_media.c +++ b/meow_ui_media.c @@ -30,6 +30,11 @@ static pthread_t g_thr; static int g_thr_on = 0; static volatile int g_quit = 0; static meow_media_playctl_fn g_cb = NULL; +/* 本进程最后一次经 meow_ui_media_meta 上报的 state(0=停 1=播放 2=暂停 3=缓冲)。 + * 供「物理播放/暂停键」做切换判据:播放中→暂停(2),否则→播放(1)。与 launcher + * now_playing.c 的同款判定(state==PLAYING ? 2 : 1)对齐。 */ +static int g_last_state = 0; +#define MEOW_UI_MEDIA_STATE_PLAYING 1 /* 后台线程主体(定义在文件后段;on_playctl 懒起时引用)。 */ static void *media_thread_main(void *arg); @@ -177,6 +182,7 @@ void meow_ui_media_meta(uint32_t state, uint32_t pos_ms, uint32_t dur_ms, memset(&mm, 0, sizeof mm); pthread_mutex_lock(&g_mu); media_app_id(); + g_last_state = (int)state; /* 记录最近上报 state:供物理播放/暂停键切换判据(见 sys_key) */ snprintf(mm.app_id, sizeof mm.app_id, "%s", g_app); mm.state = state; mm.pos_ms = pos_ms; @@ -207,6 +213,64 @@ void meow_ui_media_stop(void) pthread_mutex_unlock(&g_mu); } +/* ---- 物理媒体键 → 系统语义(M2.xx:SDK 输入层接管,App 前台也生效)---- + * 物理键是系统资源(APPS.md §3.4.1):播放/暂停/上一曲/下一曲 → 转发到当前媒体会话。 + * 本进程注册过 playctl 回调(= 本 App 是媒体源)→ 直接本地回调(最简,无 IPC 往返); + * 否则经 IPC 发 MSG_PLAYCTL 给 launcher 转发到「最近活跃媒体会话」。best-effort 绝不崩。 */ + +/* 物理媒体键 keycode → MSG_PLAYCTL cmd(1=播放 2=暂停 3=上一首 4=下一首 5=seek)。 + * 播放/暂停(164)按最近上报 state 切换(播放中→暂停,否则→播放);上一曲 165/19→3,下一曲 163→4。 + * 非媒体键返回 -1。last_state 由调用方在持锁下传入(读 g_last_state 快照)。 */ +static int media_key_cmd(int code, int last_state) +{ + switch (code) { + case 164: /* KEY_PLAYPAUSE:切换(依最近上报 state) */ + return (last_state == MEOW_UI_MEDIA_STATE_PLAYING) ? 2 : 1; + case 165: case 19: /* KEY_PREVIOUSSONG / 旧码 */ + return 3; + case 163: /* KEY_NEXTSONG */ + return 4; + default: + return -1; + } +} + +/* 控制器路径:发 MSG_PLAYCTL 给 launcher 转发到最近活跃媒体会话(launcher ipc_dispatch + * MSG_PLAYCTL → meow_media_playctl → 下发到当前源 fd)。 + * 专门开一条临时连接(不复用媒体源持久连接 g_fd,避免与后台线程 poll/读竞争); + * 无 launcher/断线 → 静默降级(绝不崩)。 */ +static void send_playctl_upstream(int cmd, int64_t seek_ms) +{ + int fd = connect_path(sock_path()); + if (fd < 0) return; + m_playctl_t c; + memset(&c, 0, sizeof c); + c.cmd = (uint32_t)cmd; + c.seek_ms = seek_ms; + if (send_frame(fd, MSG_PLAYCTL, 1, &c, sizeof c) != 0) { close(fd); return; } + /* 读 ACK(best-effort):带 200ms poll,防 launcher 挂死导致输入线程阻塞。 */ + struct pollfd p; + p.fd = fd; p.events = POLLIN; p.revents = 0; + if (poll(&p, 1, 200) > 0 && (p.revents & POLLIN)) { + uint8_t rbuf[128]; + uint16_t rt; + (void)read_frame(fd, &rt, rbuf, sizeof rbuf); + } + close(fd); +} + +/* 物理媒体键按下边沿(val==1 时由输入线程调用;重复/抬起不重复触发)。 */ +void meow_ui_media_sys_key(int code) +{ + pthread_mutex_lock(&g_mu); + meow_media_playctl_fn cb = g_cb; + int cmd = media_key_cmd(code, g_last_state); + pthread_mutex_unlock(&g_mu); + if (cmd <= 0) return; + if (cb) { cb(cmd, 0); return; } /* 本 App 是媒体源(注册过回调)→ 本地回调 */ + send_playctl_upstream(cmd, 0); /* 否则经 IPC 发 launcher 转发到最近活跃会话 */ +} + /* 后台线程主体:懒连 + poll 读 launcher 下发 MSG_PLAYCTL → 调 cb。 */ static void *media_thread_main(void *arg) { diff --git a/meow_ui_media.h b/meow_ui_media.h index 8bdcfae..a5ef67d 100644 --- a/meow_ui_media.h +++ b/meow_ui_media.h @@ -39,6 +39,13 @@ void meow_ui_media_meta(uint32_t state, uint32_t pos_ms, uint32_t dur_ms, /* 停止后台接收线程并断开连接(App 退出时调;幂等)。 */ void meow_ui_media_stop(void); +/* 物理媒体键 → 系统语义(SDK 输入层调用;App 前台也生效)。 + * code:164=播放/暂停、165/19=上一曲、163=下一曲。 + * 本 App 注册过 playctl 回调(媒体源)→ 本地直接回调 cb(cmd,seek_ms); + * 否则经 IPC 发 MSG_PLAYCTL 给 launcher 转发到最近活跃媒体会话。非媒体键/无源静默。 + * best-effort:无 launcher/socket → 优雅降级,绝不崩。 */ +void meow_ui_media_sys_key(int code); + #ifdef __cplusplus } #endif diff --git a/meow_ui_vol.c b/meow_ui_vol.c index 2f3cb4d..fdcff49 100644 --- a/meow_ui_vol.c +++ b/meow_ui_vol.c @@ -50,3 +50,72 @@ int meow_ui_vol_read(const char *path, const char *key, int def) while (*found == ' ' || *found == '\t') found++; /* value 左去空白;atoi 天然停于非数字 */ return atoi(found); } + +/* 改 settings.conf 一个 int 键:流式拷贝原文件到 .tmp,命中键行改写为 + * "key = value"(launcher 落盘格式,带空格),键缺失则末尾追加;rename 原子替换。 + * 保留其余行(注释/其它设置)——绝不 O_TRUNC 整文件。 */ +int meow_ui_vol_set(const char *path, const char *key, int val) +{ + if (!path || !path[0] || !key || !key[0]) return -1; + FILE *in = fopen(path, "r"); + char tmp[512]; + int kl = (int)strlen(key); + snprintf(tmp, sizeof tmp, "%s.tmp", path); + FILE *out = fopen(tmp, "w"); + if (!out) { if (in) fclose(in); return -1; } + + int found = 0; + char line[256]; + while (in && fgets(line, sizeof line, in)) { + char *eq = strchr(line, '='); + char *k = line; + while (*k == ' ' || *k == '\t') k++; + int n = eq ? (int)(eq - k) : 0; + while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--; + if (eq && n == kl && strncmp(k, key, (size_t)kl) == 0) { + fprintf(out, "%s = %d\n", key, val); /* 命中:改写为规范格式 */ + found = 1; + } else { + fputs(line, out); /* 其它行原样保留 */ + } + } + if (in) fclose(in); + if (!found) fprintf(out, "%s = %d\n", key, val); + fclose(out); + + if (rename(tmp, path) != 0) { remove(tmp); return -1; } + return 0; +} + +/* 音量步进(系统语义,与 launcher 设置页/watchdog 同源:volume_step/volume_max 约束)。 + * 只做 settings 逻辑 + 持久化;DAC 写入由调用方(meow_ui_vol_dac_write)负责。 */ +int meow_ui_vol_step(const char *path, int dir) +{ + int vol = meow_ui_vol_read(path, "audio.volume", MEOW_UI_VOL_DEFAULT); + int vol_max= meow_ui_vol_read(path, "audio.volume_max", MEOW_UI_VOL_MAX_DEF); + int step = meow_ui_vol_read(path, "audio.volume_step", 1); + if (step < 1) step = 1; + if (step > 5) step = 5; /* 与 watchdog 读取口径一致(1..5) */ + if (dir < 0) dir = -1; + else if (dir > 0) dir = 1; + else return vol; + + int v = meow_ui_vol_clamp(vol + dir * step, vol_max); + if (v == vol) return vol; /* 已在上下限:无进展,不写 */ + meow_ui_vol_set(path, "audio.volume", v); + return v; +} + +/* 真实 DAC 音量(CS43198, numid 1/2, 0-255),pct→255 映射直写两声道。 + * 与 launcher meow_dac_vol_write / watchdog wd_vol_write 完全同路径(语义同源)。 */ +void meow_ui_vol_dac_write(int pct) +{ + int dac = pct * 255 / 100; + if (dac < 0) dac = 0; + if (dac > 255) dac = 255; + char cmd[160]; + snprintf(cmd, sizeof cmd, + "amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1", + dac, dac); + system(cmd); +} diff --git a/meow_ui_vol.h b/meow_ui_vol.h index fa28c42..d0c71c1 100644 --- a/meow_ui_vol.h +++ b/meow_ui_vol.h @@ -29,4 +29,22 @@ int meow_ui_vol_clamp(int vol, int vol_max); * path 由调用方传入(设备传导 MEOW_SETTINGS_FILE;宿主单测指向临时文件)。 */ int meow_ui_vol_read(const char *path, const char *key, int def); +/* 改 settings.conf 里一个 int 键(地址更新/新增;**保留其余行**——与 launcher 落盘格式 + * "key = value"(带空格)一致;键缺失则追加)。用临时文件 + rename 原子替换,失败返回 -1。 + * 用途:SDK 输入层调系统音量时写 audio.volume(绝不像 watchdog wd_vol_save 那样整文件 + * O_TRUNC,避免删掉 display.* 等其它设置)。 */ +int meow_ui_vol_set(const char *path, const char *key, int val); + +/* 读 audio.volume + audio.volume_max + audio.volume_step,按 dir(±1)迈一步(绝对跨度 + * = volume_step),clamp 到 [0, MEOW_UI_VOL_MAX] 再 clamp 到 volume_max(同 meow_ui_vol_clamp + * 语义,与 launcher 设置页/watchdog 一致),经 meow_ui_vol_set 持久化到 audio.volume。 + * 返回写入 DAC 的映射值(= 夹后新卷值);已在上下限未变则返回当前值(不写)。 + * 仅做 settings 逻辑(不改 DAC);DAC 写入由调用方用 meow_ui_vol_dac_write 做。 */ +int meow_ui_vol_step(const char *path, int dir); + +/* 写真实 DAC(CS43198, numid 1/2, pct→0-255),与 launcher meow_dac_vol_write / watchdog + * wd_vol_write 同路径(amixer cset = SNDRV_CTL_IOCTL_ELEM_WRITE)。无副作用前提:宿主上 + * amixer 不存在 → system() 静默失败(拼音量的宿主单测只测 meow_ui_vol_step,不测写 DAC)。 */ +void meow_ui_vol_dac_write(int pct); + #endif /* MEOW_UI_VOL_H */ diff --git a/test_sdk_input_keys b/test_sdk_input_keys new file mode 100755 index 0000000..15f72da Binary files /dev/null and b/test_sdk_input_keys differ diff --git a/test_sdk_input_keys.c b/test_sdk_input_keys.c new file mode 100644 index 0000000..bf93ee0 --- /dev/null +++ b/test_sdk_input_keys.c @@ -0,0 +1,123 @@ +/* + * test_sdk_input_keys.c — M2.55 宿主验证:SDK 输入层接管系统按键语义(App 前台也生效)。 + * + * 覆盖(直接调输入层语义函数,等效"模拟按键事件"): + * K1 音量 +/step:audio.volume 迈一步(volume_step 约束),settings.conf 落到值改变; + * K2 音量 ± clamp:volume_max 软上限、0 下限、硬上限 100;到限不再写; + * K3 保留其它键:改 audio.volume 不整文件 O_TRUNC(display.brightness/注释保留); + * K4 媒体键本地回调:注册 playctl 回调后,sys_key(上一曲/下一曲/播放暂停) → 回调 cmd 正确 + * (播放/暂停按最近上报 state 切换:播放中→暂停(2),暂停/停止→播放(1)); + * K5 媒体键 IPC 兜底:未注册回调(g_cb==NULL)→ 走 IPC 发 launcher(无 socket → 静默不崩)。 + * + * 编译(宿主 gcc,仅链 vol/media 两个 TU;无需 meow_ui 其它子系统): + * gcc -Wall -Wextra -pthread -o test_sdk_input_keys test_sdk_input_keys.c \ + * meow_ui_vol.c meow_ui_media.c -I. -I../meow_launcherd + */ +#include "meow_ui_vol.h" +#include "meow_ui_media.h" + +#include +#include +#include +#include +#include + +static int failures = 0; +static void check(const char *name, int cond) +{ + printf("%s %s\n", cond ? "PASS" : "FAIL", name); + if (!cond) failures++; +} + +static char g_path[64]; +static void wfile(const char *content) +{ + FILE *f = fopen(g_path, "w"); + if (!f) { perror("fopen"); exit(2); } + fputs(content, f); + fclose(f); +} +static int vol_in_file(void) +{ + return meow_ui_vol_read(g_path, "audio.volume", -1); +} + +/* —— 媒体回调捕获 —— */ +static int cb_calls = 0, cb_cmd = 0; +static int64_t cb_seek = 0; +static void playctl_cb(int cmd, int64_t seek_ms) +{ cb_calls++; cb_cmd = cmd; cb_seek = seek_ms; } + +int main(void) +{ + snprintf(g_path, sizeof g_path, "/tmp/meow_kb_%d.conf", (int)getpid()); + char sock[64]; /* 指向不存在路径:让 IPC 兜底路径确定性静默失败 */ + snprintf(sock, sizeof sock, "/tmp/meow_kb_%d.sock", (int)getpid()); + setenv("MEOW_IPC_SOCK", sock, 1); + + /* ---- K1 音量迈一步(volume_step=1 默认)---- */ + wfile("audio.volume = 60\naudio.volume_max = 100\naudio.volume_step = 1\n"); + check("K1 vol 60 +1 -> 61", meow_ui_vol_step(g_path, +1) == 61); + check("K1 settings.conf 落到 61", vol_in_file() == 61); + check("K1 vol 61 -1 -> 60", meow_ui_vol_step(g_path, -1) == 60); + check("K1 settings.conf 回到 60", vol_in_file() == 60); + + /* ---- K2 步进 volume_step=4 + clamp ---- */ + wfile("audio.volume = 60\naudio.volume_max = 100\naudio.volume_step = 4\n"); + check("K2 step=4: 60 +1 -> 64", meow_ui_vol_step(g_path, +1) == 64); + wfile("audio.volume = 99\naudio.volume_max = 100\naudio.volume_step = 4\n"); + check("K2 step=4: 99 +1 -> 100 (硬上限)", meow_ui_vol_step(g_path, +1) == 100); + + /* volume_max 软上限 */ + wfile("audio.volume = 60\naudio.volume_max = 55\naudio.volume_step = 1\n"); + meow_ui_vol_step(g_path, +1); + check("K2 volume_max=55: vol -> 55 (clamp 上限)", vol_in_file() == 55); + + /* 到上限不再写 */ + wfile("audio.volume = 100\naudio.volume_max = 100\naudio.volume_step = 1\n"); + check("K2 at hard max: +1 -> 100 (no write)", meow_ui_vol_step(g_path, +1) == 100); + wfile("audio.volume = 0\naudio.volume_max = 100\naudio.volume_step = 1\n"); + check("K2 at min: -1 -> 0 (no write)", meow_ui_vol_step(g_path, -1) == 0); + check("K2 at min settings 保持 0", vol_in_file() == 0); + + /* ---- K3 保留其它键(不整文件 O_TRUNC)---- */ + wfile("# meow-settings 1\ndisplay.brightness = 100\naudio.volume = 60\n"); + meow_ui_vol_step(g_path, +1); + check("K3 vol 60 -> 61", vol_in_file() == 61); + check("K3 保留其它键 display.brightness=100", + meow_ui_vol_read(g_path, "display.brightness", -1) == 100); + + /* ---- K5 媒体键 IPC 兜底:未注册回调 → 走 IPC(无 socket 静默不崩)---- */ + cb_calls = 0; + meow_ui_media_sys_key(165); /* 上一曲:g_cb==NULL 走 IPC 兜底,不崩 */ + check("K5 未注册回调不崩", cb_calls == 0); + + /* ---- K4 媒体键本地回调:注册后按 cmd 分发 ---- */ + meow_ui_media_on_playctl(playctl_cb); + meow_ui_media_sys_key(165); /* 上一曲 */ + check("K4 上一曲(165) -> cb(3)", cb_calls == 1 && cb_cmd == 3 && cb_seek == 0); + meow_ui_media_sys_key(163); /* 下一曲 */ + check("K4 下一曲(163) -> cb(4)", cb_calls == 2 && cb_cmd == 4); + meow_ui_media_sys_key(19); /* 旧码上一曲 */ + check("K4 旧码(19) -> cb(3)", cb_calls == 3 && cb_cmd == 3); + + /* 播放/暂停按最近上报 state 切换 */ + meow_ui_media_meta(1, 0, 0, "t", "a", "al", NULL, NULL); /* state=PLAYING */ + meow_ui_media_sys_key(164); + check("K4 播放中按播放/暂停 -> cb(2) 暂停", cb_calls == 4 && cb_cmd == 2); + meow_ui_media_meta(2, 0, 0, "t", "a", "al", NULL, NULL); /* state=PAUSED */ + meow_ui_media_sys_key(164); + check("K4 暂停中按播放/暂停 -> cb(1) 播放", cb_calls == 5 && cb_cmd == 1); + meow_ui_media_meta(0, 0, 0, "t", "a", "al", NULL, NULL); /* state=STOPPED */ + meow_ui_media_sys_key(164); + check("K4 停止中按播放/暂停 -> cb(1) 播放", cb_calls == 6 && cb_cmd == 1); + + meow_ui_media_stop(); + + unlink(g_path); + unlink(sock); + + printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS", + failures, failures == 1 ? "" : "s"); + return failures ? 1 : 0; +}