fix(sdk+launcher): 图标不渲染真根因(agent-219) — meow_ui_img.c scale_buf cover 采样把乘写成除(÷缩放比→采到源图左上角透明区,全丢); 改乘+居中裁剪偏移修正; 宿主旧测试用纯色图掩盖了该bug。+ main_menu App退出/结构变化重绘时 fb_pan_buf0_force 无条件pan回buffer0(防App翻页留buffer1致菜单残帧)

meow_ui.a→3e0ee8f7 launcher→9d0efc2f hello→9609ea6e files→90f5cf07 ncm→be8be221(均已上卡); 真机截图证据 .tmp/meowbug/fix1.png+fix_b2_after.png
This commit is contained in:
MeowOS committed 2026-08-31 15:42:24 +08:00
1 parent 05a19a9ea3
commit ab4da09e75
45 files changed
+1926 -147

No files matched your search

+12 -8
View File
@@ -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);
+68 -10
View File
@@ -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 <stdio.h>
#include <stdlib.h>
@@ -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) { /* 抬起 */
+64
View File
@@ -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)
{
+7
View File
@@ -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
+69
View File
@@ -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 键:流式拷贝原文件到 <path>.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);
}
+18
View File
@@ -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 */
BIN
View File
Binary file not shown.
+123
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
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;
}