dmenu: implement built-in zero-lag in-place multi-tap T9 input

This commit is contained in:
Luxferre
2026-10-25 14:44:17 +02:00
parent e77be498bf
commit ef18a33c9b
3 changed files with 308 additions and 15 deletions
Binary file not shown.
Binary file not shown.
+308 -15
View File
@@ -78,11 +78,25 @@ static struct item *matches, *matchend;
static struct item *prev, *curr, *next, *sel, *nav;
static int mon = -1, screen;
static int use_text_input = 0; /* selection/output mode: with -t, Shift inverts item vs typed text */
static int engage_ime = 1; /* engage fcitx (XIM) for text entry by default so the T9 input method activates on dmenu prompts */
static int engage_ime = 0; /* built-in zero-lag multi-tap T9 by default; -ime enables XIM */
static int initialindex = 0;
static int managed = 0;
static int curpos = 0, oldcurlen = 0;
enum { T9_LOWER = 0, T9_SHIFT = 1, T9_UPPER = 2, T9_SYM = 3, T9_NUM = 4 };
#define T9_KEY_STAR 10
#define T9_KEY_POUND 11
static int t9_mode = T9_LOWER;
static int t9_prev_mode = T9_LOWER;
static int t9_last_key = -1;
static int t9_tap_idx = 0;
static int t9_last_upper = 0;
static long long t9_last_ms = 0;
static int t9_held_key = -1;
static long long t9_held_since_ms = 0;
static int t9_longpress_done = 0;
static int daemon_mode = 0;
static int menu_active = 0;
static int menu_mode = MODE_NONE;
@@ -232,13 +246,58 @@ add_menu_item(const char *label, const char *action, int loop)
items[items_count].right = NULL;
}
static const char *
t9_mode_badge(int mode)
{
switch (mode) {
case T9_SHIFT: return "[Abc]";
case T9_UPPER: return "[ABC]";
case T9_SYM: return "[Sym]";
case T9_NUM: return "[123]";
default: return "[abc]";
}
}
static void
update_t9_status_indicator(void)
{
const char *rdir = getenv("XDG_RUNTIME_DIR");
const char *base = (rdir && rdir[0]) ? rdir : "/run/user/10000";
const char *badge = t9_mode_badge(t9_mode);
char path[256];
snprintf(path, sizeof(path), "%s/sxmo_t9_mode", base);
FILE *fp = fopen(path, "w");
if (fp) {
fprintf(fp, "%s\n", badge);
fclose(fp);
}
snprintf(path, sizeof(path), "%s/sxmo_status/default/005-input-method", base);
fp = fopen(path, "w");
if (fp) {
fprintf(fp, "%s\n", badge);
fclose(fp);
}
}
static void
reset_t9_state(void)
{
t9_last_key = -1;
t9_tap_idx = 0;
t9_last_ms = 0;
t9_held_key = -1;
t9_held_since_ms = 0;
t9_longpress_done = 0;
}
static void
reset_session_defaults(int case_insensitive, const char *new_prompt)
{
free_items();
topbar = 1;
centered = 0;
use_text_input = 0; /* reset selection/output mode; fcitx routing (engage_ime) stays on */
use_text_input = 0;
initialindex = 0;
fstrncmp = case_insensitive ? strncasecmp : strncmp;
fstrstr = case_insensitive ? cistrstr : strstr;
@@ -250,6 +309,11 @@ reset_session_defaults(int case_insensitive, const char *new_prompt)
curpos = 0;
oldcurlen = 0;
inputw = 0;
reset_t9_state();
if (prompt && (strstr(prompt, "PIN") || strstr(prompt, "PUK")))
t9_mode = T9_NUM;
else
t9_mode = T9_LOWER;
}
static void
@@ -379,10 +443,18 @@ recalculatenumbers(void)
}
for (item = items; item && item->text; item++)
denom++;
if (numer == denom)
snprintf(numbers, NUMBERSBUFSIZE, "%d", numer);
else
snprintf(numbers, NUMBERSBUFSIZE, "%d/%d", numer, denom);
if (!engage_ime) {
const char *badge = t9_mode_badge(t9_mode);
if (numer == denom)
snprintf(numbers, NUMBERSBUFSIZE, "%s %d", badge, numer);
else
snprintf(numbers, NUMBERSBUFSIZE, "%s %d/%d", badge, numer, denom);
} else {
if (numer == denom)
snprintf(numbers, NUMBERSBUFSIZE, "%d", numer);
else
snprintf(numbers, NUMBERSBUFSIZE, "%d/%d", numer, denom);
}
}
static void
@@ -1184,6 +1256,166 @@ spawn_xdrawer_and_close(void)
trigger_plugin_open("xdrawer");
}
static int
map_t9_key(XKeyEvent *ev)
{
if (ev->state & (ControlMask | Mod1Mask | Mod4Mask))
return -1;
KeySym ks = XLookupKeysym(ev, 0);
unsigned int kc = ev->keycode;
if (kc == 530 || kc == 63 || ks == XK_asterisk ||
ks == XK_KP_Multiply || (unsigned int)ks == 0x1008ffaa)
return T9_KEY_STAR;
if (kc == 531 || kc == 106 || ks == XK_numbersign ||
ks == XK_KP_Divide || (unsigned int)ks == 0x1008ffab)
return T9_KEY_POUND;
if (kc >= 10 && kc <= 18)
return (int)(kc - 9);
if (kc == 19)
return 0;
if (ks >= XK_0 && ks <= XK_9)
return (int)(ks - XK_0);
if (ks >= XK_KP_0 && ks <= XK_KP_9)
return (int)(ks - XK_KP_0);
return -1;
}
static const char *
get_t9_chars(int key_id, int mode, int upper)
{
if (mode == T9_SYM) {
switch (key_id) {
case 0: return " 0";
case 1: return ".,!?:;-_1";
case 2: return "@#$%^&2";
case 3: return "()[]{}<>3";
case 4: return "+-*/=_4";
case 5: return "|\\~`'\"5";
case 6: return "'\"`~^6";
case 7: return "/\\|_-+7";
case 8: return "<>!=?&8";
case 9: return "$%^&*#@9";
case T9_KEY_STAR: return "*#+-=_/\\|~`@$%^&()[]{}<>'\".,!?:;";
default: return "";
}
}
switch (key_id) {
case 0: return " 0";
case 1: return ".,-_!?/@:;'\"+=*#$%^&()[]{}<>|\\~`1";
case 2: return upper ? "ABC2abc" : "abc2ABC";
case 3: return upper ? "DEF3def" : "def3DEF";
case 4: return upper ? "GHI4ghi" : "ghi4GHI";
case 5: return upper ? "JKL5jkl" : "jkl5JKL";
case 6: return upper ? "MNO6mno" : "mno6MNO";
case 7: return upper ? "PQRS7pqrs" : "pqrs7PQRS";
case 8: return upper ? "TUV8tuv" : "tuv8TUV";
case 9: return upper ? "WXYZ9wxyz" : "wxyz9WXYZ";
case T9_KEY_STAR: return "*#+-=_/\\|~`@$%^&()[]{}<>'\".,!?:;";
default: return "";
}
}
static void
handle_t9_longpress(int key_id)
{
if (key_id < 0 || t9_longpress_done)
return;
t9_longpress_done = 1;
t9_last_key = -1;
t9_tap_idx = 0;
if (key_id >= 0 && key_id <= 9) {
if (t9_mode != T9_NUM && cursor > 0) {
text[cursor - 1] = (char)('0' + key_id);
match();
if (menu_active || !daemon_mode)
drawmenu();
}
} else if (key_id == T9_KEY_STAR) {
if (t9_mode == T9_NUM && cursor > 0) {
text[cursor - 1] = '.';
match();
if (menu_active || !daemon_mode)
drawmenu();
}
} else if (key_id == T9_KEY_POUND) {
t9_mode = t9_prev_mode;
update_t9_status_indicator();
insert("#", 1);
if (menu_active || !daemon_mode)
drawmenu();
}
}
static void
check_t9_longpress_timeout(void)
{
if (engage_ime || t9_held_key < 0 || t9_longpress_done || t9_held_since_ms <= 0)
return;
if (dmenu_now_ms() - t9_held_since_ms >= 500)
handle_t9_longpress(t9_held_key);
}
static void
handle_t9_keypress(int key_id)
{
long long now = dmenu_now_ms();
if (t9_held_key == key_id) {
if (!t9_longpress_done && (now - t9_held_since_ms) >= 500)
handle_t9_longpress(key_id);
return;
}
t9_held_key = key_id;
t9_held_since_ms = now;
t9_longpress_done = 0;
if (key_id == T9_KEY_POUND) {
t9_last_key = -1;
t9_tap_idx = 0;
t9_prev_mode = t9_mode;
t9_mode = (t9_mode + 1) % 5;
update_t9_status_indicator();
if (menu_active || !daemon_mode)
drawmenu();
return;
}
if (t9_mode == T9_NUM) {
char ch = (key_id == T9_KEY_STAR) ? '*' : (char)('0' + key_id);
t9_last_key = -1;
t9_tap_idx = 0;
insert(&ch, 1);
if (menu_active || !daemon_mode)
drawmenu();
return;
}
int same_key = (key_id == t9_last_key && (now - t9_last_ms) <= 850 && cursor > 0);
if (same_key) {
const char *chars = get_t9_chars(key_id, t9_mode, t9_last_upper);
size_t clen = strlen(chars);
if (clen > 0) {
t9_tap_idx = (t9_tap_idx + 1) % (int)clen;
text[cursor - 1] = chars[t9_tap_idx];
match();
}
} else {
t9_last_key = key_id;
t9_tap_idx = 0;
t9_last_upper = (t9_mode == T9_SHIFT || t9_mode == T9_UPPER);
const char *chars = get_t9_chars(key_id, t9_mode, t9_last_upper);
if (t9_mode == T9_SHIFT) {
t9_mode = T9_LOWER;
update_t9_status_indicator();
}
if (chars[0])
insert(&chars[0], 1);
}
t9_last_ms = now;
if (menu_active || !daemon_mode)
drawmenu();
}
static void
keypress(XKeyEvent *ev)
{
@@ -1192,6 +1424,15 @@ keypress(XKeyEvent *ev)
KeySym ksym;
Status status;
if (!engage_ime) {
int t9k = map_t9_key(ev);
if (t9k >= 0) {
handle_t9_keypress(t9k);
return;
}
reset_t9_state();
}
ksym = NoSymbol;
if (xic && ev->keycode == 0)
len = Xutf8LookupString(xic, ev, buf, sizeof buf, &ksym, &status);
@@ -2679,6 +2920,9 @@ show_menu(void)
soft_pending = 0;
soft_deadline = 0;
reset_t9_state();
if (!engage_ime)
update_t9_status_indicator();
if (!menu_active) {
menu_active = 1;
@@ -2733,6 +2977,7 @@ hide_menu(void)
mark_menu_active(0);
soft_pending = 0;
soft_deadline = 0;
reset_t9_state();
if (xic && engage_ime) {
char *r = Xutf8ResetIC(xic);
@@ -2855,6 +3100,10 @@ handle_client_socket(int sock_fd)
fstrstr = cistrstr;
} else if (!strcmp(c_argv[i], "-t"))
use_text_input = 1;
else if (!strcmp(c_argv[i], "-ime"))
engage_ime = 1;
else if (!strcmp(c_argv[i], "-noime"))
engage_ime = 0;
else if (!strcmp(c_argv[i], "-l") && i + 1 < actual_argc)
lines = atoi(c_argv[++i]);
else if (!strcmp(c_argv[i], "-H") && i + 1 < actual_argc)
@@ -2865,6 +3114,8 @@ handle_client_socket(int sock_fd)
free(session_prompt);
session_prompt = strdup(c_argv[++i]);
prompt = session_prompt;
if (prompt && (strstr(prompt, "PIN") || strstr(prompt, "PUK")))
t9_mode = T9_NUM;
} else if ((!strcmp(c_argv[i], "-fn") || !strcmp(c_argv[i], "-nb") ||
!strcmp(c_argv[i], "-nf") || !strcmp(c_argv[i], "-sb") ||
!strcmp(c_argv[i], "-sf") || !strcmp(c_argv[i], "-w")) &&
@@ -2995,6 +3246,28 @@ process_x11_events(void)
if (!initial_released || dmenu_now_ms() < start_ignore)
continue;
}
if (!engage_ime && ev.type == KeyRelease) {
int t9k = map_t9_key(&ev.xkey);
if (t9k >= 0) {
if (XEventsQueued(dpy, QueuedAfterReading) > 0) {
XEvent nev;
XPeekEvent(dpy, &nev);
if (nev.type == KeyPress && nev.xkey.time == ev.xkey.time &&
nev.xkey.keycode == ev.xkey.keycode) {
XNextEvent(dpy, &nev);
if (!t9_longpress_done && (dmenu_now_ms() - t9_held_since_ms) >= 450)
handle_t9_longpress(t9k);
continue;
}
}
if (t9_held_key == t9k) {
t9_held_key = -1;
t9_held_since_ms = 0;
t9_longpress_done = 0;
}
continue;
}
}
if (engage_ime && !issoftkey(ev.xkey.keycode) && !(is_enter && items)) {
ev.xkey.window = win;
if (XFilterEvent(&ev, None))
@@ -3025,7 +3298,7 @@ process_x11_events(void)
if (any_other_plugin_active()) {
if (daemon_mode)
cancel_session();
} else {
} else if (ev.xvisibility.state == VisibilityFullyObscured) {
XRaiseWindow(dpy, win);
grabkeyboard();
drw_map(drw, win, 0, 0, mw, mh);
@@ -3084,6 +3357,9 @@ run_standalone(void)
int xfd = ConnectionNumber(dpy);
soft_pending = 0;
soft_deadline = 0;
reset_t9_state();
if (!engage_ime)
update_t9_status_indicator();
start_ignore = dmenu_now_ms() + 40;
initial_released = (query_softkeys_down() == 0);
read_softkey_state(&init_soft_mask, &init_soft_ms);
@@ -3091,14 +3367,20 @@ run_standalone(void)
for (;;) {
process_x11_events();
check_softkey_timeout();
check_t9_longpress_timeout();
int timeout_ms = initial_released ? -1 : 40;
if (soft_pending && soft_deadline > 0) {
long long rem = MAX(1LL, soft_deadline - dmenu_now_ms());
struct pollfd pfd = { xfd, POLLIN, 0 };
poll(&pfd, 1, (int)rem);
} else {
struct pollfd pfd = { xfd, POLLIN, 0 };
poll(&pfd, 1, initial_released ? -1 : 40);
int rem = (int)MAX(1LL, soft_deadline - dmenu_now_ms());
if (timeout_ms < 0 || rem < timeout_ms)
timeout_ms = rem;
}
if (!engage_ime && t9_held_key >= 0 && !t9_longpress_done && t9_held_since_ms > 0) {
int rem = (int)MAX(1LL, (t9_held_since_ms + 500) - dmenu_now_ms());
if (timeout_ms < 0 || rem < timeout_ms)
timeout_ms = rem;
}
struct pollfd pfd = { xfd, POLLIN, 0 };
poll(&pfd, 1, timeout_ms);
}
}
@@ -3109,8 +3391,10 @@ run_daemon_loop(int fifo_fd, int sock_fd)
for (;;) {
process_x11_events();
if (menu_active)
if (menu_active) {
check_softkey_timeout();
check_t9_longpress_timeout();
}
int timeout_ms = -1;
if (menu_active) {
@@ -3118,6 +3402,11 @@ run_daemon_loop(int fifo_fd, int sock_fd)
timeout_ms = (int)MAX(1LL, soft_deadline - dmenu_now_ms());
else if (!initial_released)
timeout_ms = 40;
if (!engage_ime && t9_held_key >= 0 && !t9_longpress_done && t9_held_since_ms > 0) {
int rem = (int)MAX(1LL, (t9_held_since_ms + 500) - dmenu_now_ms());
if (timeout_ms < 0 || rem < timeout_ms)
timeout_ms = rem;
}
}
struct pollfd pfds[3];
@@ -3303,7 +3592,7 @@ setup(int map_immediately)
grabkeyboard();
grabfocus();
}
if (xic)
if (xic && engage_ime)
XSetICFocus(xic);
}
}
@@ -3375,6 +3664,10 @@ dmenu_run_main(int argc, char *argv[])
fstrstr = cistrstr;
} else if (!strcmp(argv[i], "-t"))
use_text_input = 1;
else if (!strcmp(argv[i], "-ime"))
engage_ime = 1;
else if (!strcmp(argv[i], "-noime"))
engage_ime = 0;
else if (!strcmp(argv[i], "-wm"))
managed = 1;
else if (i + 1 == argc)