/* keys.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ #include "meow_launcher_internal.h" int kd_tick(struct kd_state *s, int raw) { unsigned long long now = now_ms(); int ev = KD_NONE; if (raw != s->raw) { if (s->raw == 0 && raw != 0) { /* 任何跳变都重记稳定窗口(含原始 raw 变化) */ } s->raw = raw; s->since = now; } int want = s->raw; if (want != s->stable) { if (now - s->since >= KEY_DEBOUNCE_MS) { s->stable = want; if (s->stable && !s->down) { s->down = 1; s->down_at = now; s->longpressed = 0; ev = KD_DOWN; } else if (!s->stable && s->down) { s->down = 0; ev = KD_UP; } } } if (s->down && !s->longpressed && now - s->down_at >= CLICK_MS) s->longpressed = 1; return ev; } /* Unblank the panel AFTER the player is frozen: with the player SIGSTOPped * it can no longer re-blank, so the menu (drawn on fb0) stays visible even * when the menu opened while the player's screen timeout had blanked the * panel. The blank state still belongs to the player's state machine — on * session end restore_backlight() re-blanks when menu_opened_blank, so the * player resumes into a consistent (blanked) state. */ void unblank_panel(void) { int fbfd = open(FB_DEV, O_RDWR); if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); } logmsg("panel unblanked (player frozen)"); } void meow_panel_off(void) { meow_saved_brightness = read_brightness(); if (meow_saved_brightness <= 0) meow_saved_brightness = 60; write_brightness(0, "meow_panel_off"); if (screen_off_mode == 0) { /* panel:面板下电 + FBIOPBLANK(4)(同时挂起 goodix 触摸,无误触) */ int f = open(FB_DEV, O_RDWR); if (f >= 0) { ioctl(f, FBIOBLANK, 4); close(f); } } else { /* black:仅背光 0 全黑(面板仍上电,触摸可唤醒) */ } meow_screen_on = 0; logmsg("meow panel OFF (mode=%s brightness 0%s)", screen_off_mode ? "black" : "panel", screen_off_mode ? "" : " + blank 4"); } void meow_panel_on(void) { int f = open(FB_DEV, O_RDWR); if (f >= 0) { ioctl(f, FBIOBLANK, 0); close(f); } /* 亮度恢复以**用户设置**为准,而非 meow_saved_brightness(可能过期)。 * watchdog 独立息屏(wd_panel_off 直接 write_brightness(0),见 meow_watchdog.c)**不会** * 更新本进程 meow_saved_brightness——若用其恢复会回错值(真机「PIN 亮屏亮度跳高」, * 2026-09-03)。settings 已从盘上读到→按 display.brightness(clamp)恢复;未读到(早启动) * →回退 meow_saved_brightness 现值(保持旧行为,防开机竞态)。 */ if (meow_settings_loaded()) { int b = meow_settings_brightness_clamped(); if (b > 0) { write_brightness(b, "meow_panel_on"); meow_saved_brightness = b; } } else { write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60, "meow_panel_on"); } meow_screen_on = 1; /* M2.8 Bug 3-residual:亮屏必须用 fb_pan_buf0_force 无条件重发 pan。fb_norm_buf0 在 * 缓存 fb_active_yoffset==0 时提前返回,而 FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位 * 到非 buffer0 / 失效位置,缓存仍是 0 导致真正的 re-pan 从未发出 —— M2.7 只加 fb_norm_buf0 * 仍黑屏的原因。主界面都画在 buffer0(mmap 偏移 0),此处强制 re-pan 保证唤醒后落在 buffer0。 * 当前界面内容的重绘由调用方(run_menu 醒屏分支)经 meow_repaint_surface 完成。 */ fb_pan_buf0_force(); logmsg("meow panel ON (blank 0 + brightness %d)", meow_saved_brightness); } void meow_toggle_panel(void) { if (meow_screen_on) meow_panel_off(); else meow_panel_on(); } int kp_open(struct keypoll *kp, const char *dev) { memset(kp, 0, sizeof *kp); kp->fd = open(dev, O_RDONLY | O_NONBLOCK); if (kp->fd < 0) { logmsg("open %s: %s", dev, strerror(errno)); return -1; } if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) { logmsg("EVIOCGKEY %s: %s", dev, strerror(errno)); close(kp->fd); return -1; } memcpy(kp->prev, kp->cur, sizeof kp->cur); kp->ok = 1; logmsg("keypoll %s ready", dev); return 0; } /* 关闭 keypoll(设置页/下拉框等自开设备用;幂等,ok=0 时不动) */ void kp_close(struct keypoll *kp) { if (kp->ok && kp->fd >= 0) close(kp->fd); kp->ok = 0; } /* Poll device key state; call cb(code, is_down) for each edge. * Returns 1 if any edge. */ int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx), void *ctx) { if (!kp->ok) return 0; if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) return 0; int changed = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp->cur[w] == kp->prev[w]) continue; for (int b = 0; b < 32; b++) { int c = (kp->cur[w] >> b) & 1; int p = (kp->prev[w] >> b) & 1; if (c != p) { int code = w * 32 + b; changed = 1; if (cb) cb(code, c, ctx); } } kp->prev[w] = kp->cur[w]; } return changed; } int kp_is_down(const struct keypoll *k, int code) { return (k->cur[code / 32] >> (code % 32)) & 1; } int hotkey_mode = HOTKEY_MODE_PWRVOL; int seq_codes[MAX_SEQ_LEN]; int seq_len = 0; int seq_gap_ms = 600; static int key_name_code(const char *n) { if (!strcmp(n, "volup")) return KEY_VOLUMEUP; if (!strcmp(n, "voldown")) return KEY_VOLUMEDOWN; if (!strcmp(n, "play")) return KEY_PLAYPAUSE; if (!strcmp(n, "next")) return KEY_NEXT; if (!strcmp(n, "previous")) return KEY_PREVIOUSSONG; if (!strcmp(n, "power")) return KEY_POWER; return -1; } void load_config(void) { menu_bg = 0x000000; hotkey_mode = HOTKEY_MODE_PWRVOL; /* default: hold power + VOL_DOWN tap */ seq_len = 0; seq_gap_ms = 600; char path[256]; snprintf(path, sizeof path, "%s/meow.conf", MEOWAPPS); FILE *f = fopen(path, "r"); if (f) { char buf[128]; while (fgets(buf, sizeof buf, f)) { if (!strncmp(buf, "bg_color=", 9)) { unsigned int v; if (sscanf(buf + 9, "%x", &v) == 1) menu_bg = v & 0xFFFFFF; } else if (!strncmp(buf, "hotkey=", 7)) { char *p = buf + 7; while (*p == ' ' || *p == '\t') p++; if (!strncmp(p, "seq", 3)) hotkey_mode = HOTKEY_MODE_SEQ; else hotkey_mode = HOTKEY_MODE_PWRVOL; } else if (!strncmp(buf, "hotkey_seq=", 11)) { char *p = buf + 11; seq_len = 0; /* BUGFIX: the outer loop must consume the EOL — with * *p staying at '\n' the empty-name branch never advanced p * and the daemon spun forever (found on-device: a hotkey_seq * line wedged load_config in a busy loop). */ while (*p && *p != '\n' && *p != '\r' && seq_len < MAX_SEQ_LEN) { char name[16]; int k = 0; while (*p && *p != ',' && *p != '\n' && *p != '\r' && k < 15) name[k++] = *p++; name[k] = 0; if (k >= 15) /* malformed long name: skip to separator/EOL */ while (*p && *p != ',' && *p != '\n' && *p != '\r') p++; if (*p == ',') p++; int c = key_name_code(name); if (c > 0) seq_codes[seq_len++] = c; } } else if (!strncmp(buf, "hotkey_seq_gap=", 15)) { int g = atoi(buf + 15); if (g >= 200 && g <= 2000) seq_gap_ms = g; } } fclose(f); } if (seq_len == 0) { seq_codes[0] = KEY_VOLUMEUP; seq_len = 3; } /* seq fallback volup x3 */ if (hotkey_mode == HOTKEY_MODE_PWRVOL) logmsg("config: bg_color=%06x hotkey=power_voldown (window %d-%dms)", menu_bg, HK_PWRWIN_MIN, HK_PWRWIN_MAX); else logmsg("config: bg_color=%06x hotkey=seq (%d taps of code %d gap=%dms)", menu_bg, seq_len, seq_codes[0], seq_gap_ms); } /* ========================================================================= * 系统级音量键翻页门(READER_UI_DESIGN §E;用户 2026-09-04 拍板)。 * * 语义:音量键是系统资源(默认调音量)。本门在「当前台 App 为其 app.json 声明 * volume_page 的应用(MVP:os.meow.reader)」时,把 VOL_UP/VOL_DOWN 转换成系统翻页 * 信号(MSG_KEY_PAGEUP/PAGEDOWN,meow_ipc.h)经 IPC 下发给该前台 App;其余情况一概 * 放行(return 0),音量基本行为/插孔消抖/音量反转等绝不破坏。 * * 职责分离(关键,且最小侵入): * - 本模块只提供「判定 + 门」:meow_volpage_app_allows() 读 app.json 声明; * meow_volpage_key() 在「已武装 sink 且按下 VOL」时消费并发下发; * - 武装(sink 注册)= 前台应用进入体积页声明 app 时调用 meow_volpage_sink() * (应用存在连接后其 fd 生效);离开/退出时 meow_volpage_sink_clear()。 * 该接线落在「前台会话的按键路径」(app_launcher.c / meow_launcherd.c 主循环), * 属主会话统一收编范围,此处不越界改动。 * * 声明字段(app.json):`"volume_page": true`(或 1)。未来其它 App 可声明复用。 * ========================================================================= */ /* 前台翻页 sink(本模块私有;单写者=前台会话上下文,读=按键门)。 */ static char g_volpage_app[MEOW_APP_ID_MAX] = ""; static int g_volpage_fd = -1; void meow_volpage_sink_clear(void); /* 前向声明(sink 内调用) */ /* 读 //app.json 是否声明 volume_page=true|1。1=声明;0=否/读不到。 */ int meow_volpage_app_allows(const char *app_id) { if (!app_id || !app_id[0]) return 0; char path[320]; snprintf(path, sizeof path, "%s/%s/app.json", MEOWAPPS, app_id); FILE *f = fopen(path, "r"); if (!f) return 0; /* 轻量扫描:找 "volume_page" → 跳过空白 → true/1。app.json 为紧凑 JSON。 */ char buf[1024]; buf[0] = 0; size_t rd = fread(buf, 1, sizeof buf - 1, f); buf[rd] = 0; fclose(f); const char *p = buf; while ((p = strstr(p, "volume_page")) != NULL) { p += strlen("volume_page"); while (*p == ' ' || *p == '\t' || *p == ':' || *p == '"') p++; if (strncmp(p, "true", 4) == 0) return 1; if (*p == '1' && (p[1] < '0' || p[1] > '9')) return 1; /* 未命中则继续找(防字段重复);跳过本段 */ while (*p && *p != ',' && *p != '}') p++; } return 0; } /* 武装/清除 sink。app_id 非空且 fd>=0 → 记录;否则清除。幂等(同 app_id+fd 不重复记日志, * 供前台会话循环每圈安全重调,也兼容断线重连换 fd)。 */ void meow_volpage_sink(const char *app_id, int fd) { if (!app_id || !app_id[0] || fd < 0) { meow_volpage_sink_clear(); return; } if (g_volpage_fd == fd && !strcmp(g_volpage_app, app_id)) return; /* 同 app_id+fd:幂等 */ snprintf(g_volpage_app, sizeof g_volpage_app, "%s", app_id); g_volpage_fd = fd; logmsg("volpage: sink armed app=%s fd=%d", g_volpage_app, g_volpage_fd); } void meow_volpage_sink_clear(void) { if (g_volpage_fd >= 0) logmsg("volpage: sink cleared app=%s", g_volpage_app[0] ? g_volpage_app : "?"); g_volpage_app[0] = 0; g_volpage_fd = -1; } int meow_volpage_armed(void) { return g_volpage_fd >= 0; } /* 按键门:按下 VOL_UP/DOWN 且 sink 已武装 → 下发翻页信号并消费(返回 1)。 * 未武装 / 非 VOL / 抬起 → 返回 0(放行给音量路径)。 */ int meow_volpage_key(int code, int is_down) { if (!is_down) return 0; /* 只响应按下边沿 */ if (g_volpage_fd < 0) return 0; /* 未武装:音量照旧 */ if (code == KEY_VOLUMEUP) { (void)meow_ipc_send_pub(g_volpage_fd, MSG_KEY_PAGEUP, 1, NULL, 0); return 1; } if (code == KEY_VOLUMEDOWN) { (void)meow_ipc_send_pub(g_volpage_fd, MSG_KEY_PAGEDOWN, 1, NULL, 0); return 1; } return 0; }