// SPDX-License-Identifier: GPL-2.0-only // Dialer DWM Plugin - Preloaded resident X11 dialer with contact & T9 integration (240x320) #include #include #include #include #include #include #ifndef _EVDEVK #define _EVDEVK(_v) (0x10081000 + (_v)) #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "../sxmo-dwm/dwm_plugin.h" #include "phone_data.h" namespace { using namespace bananarchy; volatile sig_atomic_t gRunning = 1; void handleSignal(int) { gRunning = 0; } int dialer_run_main(int argc, char** argv) { gRunning = 1; bool isPreload = false; std::string initArg; for (int i = 1; i < argc; ++i) { if (argv[i] && std::strcmp(argv[i], "--preload") == 0) { isPreload = true; } else if (argv[i] && argv[i][0]) { initArg += argv[i]; } } // If the resident dialer daemon is already running, forward the command via FIFO if (sendPluginCommand("dialer", isPreload ? "NOP" : (initArg.empty() ? "OPEN" : ("OPEN " + initArg)))) { return 0; } std::string daemonPidPath = getPluginDaemonPidPath("dialer"); std::string activePidPath = getRuntimeFile("xdialer.pid"); writeIntFile(daemonPidPath, static_cast(::getpid())); int fifoFd = setupPluginDaemonFifo("dialer"); struct sigaction sa {}; sa.sa_handler = handleSignal; sigemptyset(&sa.sa_mask); sigaction(SIGTERM, &sa, nullptr); sigaction(SIGINT, &sa, nullptr); Display* dpy = XOpenDisplay(nullptr); if (!dpy) { if (fifoFd >= 0) ::close(fifoFd); ::unlink(daemonPidPath.c_str()); return 1; } int screen = DefaultScreen(dpy); Window root = RootWindow(dpy, screen); Visual* visual = DefaultVisual(dpy, screen); Colormap cmap = DefaultColormap(dpy, screen); XSetWindowAttributes swa{}; swa.override_redirect = True; swa.background_pixel = 0x222222; swa.border_pixel = 0x005577; swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | StructureNotifyMask | VisibilityChangeMask; Window win = XCreateWindow( dpy, root, kWinX, kWinY, kWinWidth, kWinHeight, 0, DefaultDepth(dpy, screen), CopyFromParent, visual, CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa); XClassHint ch{}; char resName[] = "xdialer"; char resClass[] = "XDialer"; ch.res_name = resName; ch.res_class = resClass; XSetClassHint(dpy, win, &ch); XStoreName(dpy, win, "Dialer"); Pixmap pix = XCreatePixmap(dpy, win, kWinWidth, kWinHeight, DefaultDepth(dpy, screen)); GC gc = XCreateGC(dpy, pix, 0, nullptr); XftDraw* xftDraw = XftDrawCreate(dpy, pix, visual, cmap); PangoFontMap* fontMap = pango_xft_get_font_map(dpy, screen); PangoContext* pangoCtx = pango_font_map_create_context(fontMap); PangoLayout* layoutNum = pango_layout_new(pangoCtx); PangoLayout* layoutMain = pango_layout_new(pangoCtx); PangoLayout* layoutSmall = pango_layout_new(pangoCtx); PangoLayout* layoutIcon = pango_layout_new(pangoCtx); PluginTheme theme; theme.load(dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon, layoutNum, win); XftColor& colWhite = theme.colWhite; XftColor& colLight = theme.colLight; XftColor& colMuted = theme.colMuted; XftColor& colYellow = theme.colYellow; XftColor& colGreen = theme.colGreen; XftColor& colBlue = theme.colBlue; ContactBook book; std::vector allContacts; std::vector filtered; FileStamp contactsStamp; FileStamp logStamp; bool contactsLoadedOnce = false; auto reloadContactsIfNeeded = [&]() -> bool { bool cChanged = contactsStamp.checkAndUpdate(getContactsFilePath()); bool lChanged = logStamp.checkAndUpdate(getModemLogFilePath()); if (cChanged || lChanged || !contactsLoadedOnce) { contactsLoadedOnce = true; book.load(); allContacts = book.buildDialerList(); return true; } return false; }; reloadContactsIfNeeded(); std::string number; int selected = -1; bool visible = false; int64_t openStartMs = 0; int64_t startupIgnoreUntilMs = 0; bool initialSoftkeysReleased = true; int softkeyPending = 0; int64_t softkeyDeadlineMs = 0; bool zeroHeld = false; int64_t zeroPressMs = 0; enum class DialerModal { Inactive = 0, Imei = 1, Ussd = 2, }; DialerModal modal = DialerModal::Inactive; DeviceImeis deviceImeis; std::string ussdCode; std::string ussdResponse; std::string ussdReplyInput; bool ussdSessionActive = false; int ussdScrollLine = 0; auto updateFilter = [&]() { filtered.clear(); if (number.empty()) { filtered = allContacts; if (selected >= static_cast(filtered.size())) { selected = filtered.empty() ? -1 : 0; } if (selected < 0 && !filtered.empty()) { selected = 0; } return; } std::string qNorm = normalizePhone(number); std::string qLower = toLowerStr(number); std::vector prefixMatches; std::vector subMatches; for (const auto& c : allContacts) { bool pfx = (c.normNumber.rfind(qNorm, 0) == 0) || (!qNorm.empty() && c.t9Name.rfind(qNorm, 0) == 0); bool sub = pfx || (c.normNumber.find(qNorm) != std::string::npos) || (!qNorm.empty() && c.t9Name.find(qNorm) != std::string::npos) || (c.lowerName.find(qLower) != std::string::npos); if (pfx) { prefixMatches.push_back(c); } else if (sub) { subMatches.push_back(c); } } filtered.insert(filtered.end(), prefixMatches.begin(), prefixMatches.end()); filtered.insert(filtered.end(), subMatches.begin(), subMatches.end()); if (selected >= static_cast(filtered.size())) { selected = -1; } }; auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, int h, const std::string& text, int maxW = -1) { pango_layout_set_text(layout, text.c_str(), static_cast(text.size())); if (maxW > 0) { pango_layout_set_width(layout, maxW * PANGO_SCALE); pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); } else { pango_layout_set_width(layout, -1); pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE); } PangoRectangle ink{}, logical{}; pango_layout_get_pixel_extents(layout, &ink, &logical); int ty = y + (h - logical.height) / 2; pango_xft_render_layout(xftDraw, color, layout, x * PANGO_SCALE, ty * PANGO_SCALE); }; auto pangoWidth = [&](PangoLayout* layout, const std::string& text) -> int { pango_layout_set_width(layout, -1); pango_layout_set_text(layout, text.c_str(), static_cast(text.size())); PangoRectangle ink{}, logical{}; pango_layout_get_pixel_extents(layout, &ink, &logical); return logical.width; }; constexpr int kHeaderH = 56; constexpr int kSoftbarH = 26; constexpr int kListY = kHeaderH; constexpr int kListH = kWinHeight - kHeaderH - kSoftbarH; constexpr int kVisibleRows = 5; auto render = [&]() { XSetForeground(dpy, gc, theme.spec.bgMain); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); bool topSel = (selected < 0); XSetForeground(dpy, gc, topSel ? theme.spec.bgInputFocus : theme.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH); if (topSel) { XSetForeground(dpy, gc, theme.spec.accent); XFillRectangle(dpy, pix, gc, 0, 0, 4, kHeaderH); } XSetForeground(dpy, gc, topSel ? theme.spec.selBg : theme.spec.border); XFillRectangle(dpy, pix, gc, 0, kHeaderH - 2, kWinWidth, 2); drawPango(layoutIcon, &colGreen, 10, 4, 30, kIconPhone); if (number.empty()) { drawPango(layoutMain, &colMuted, 32, 4, 30, "Enter number...", 196); } else { std::string dispNum = number; if (dispNum.size() > 13) { dispNum = ".." + dispNum.substr(dispNum.size() - 12); } drawPango(layoutNum, &colWhite, 32, 2, 32, dispNum, 182); drawPango(layoutIcon, &colMuted, 218, 4, 30, kIconBackspace); } std::string subText; XftColor* subCol = &colMuted; if (selected >= 0 && selected < static_cast(filtered.size())) { const auto& c = filtered[selected]; subText = "Dial: " + c.number; subCol = &colYellow; } else if (!number.empty()) { if (number == "*#06#") { subText = "Show Device IMEI (1 & 2)"; subCol = &colYellow; } else if (isUssdCode(number)) { subText = "USSD Code (Press Call/OK to send)"; subCol = &colYellow; } else { std::string exactName = book.resolveName(number); if (!exactName.empty()) { subText = exactName; subCol = &colGreen; } else if (!filtered.empty()) { subText = filtered[0].name + " (" + filtered[0].number + ")"; subCol = &colBlue; } else { subText = "Press Call to dial (Hold 0: +)"; } } } else { subText = "0-9/*/# Dial | Hold 0: +"; } drawPango(layoutSmall, subCol, 32, 33, 20, subText, 202); if (filtered.empty()) { drawPango(layoutMain, &colMuted, 16, kListY + 40, 30, number.empty() ? "No saved contacts" : "New number (press Call)", 208); } else { int total = static_cast(filtered.size()); int focusIdx = (selected >= 0) ? selected : 0; int scrollOffset = 0; if (focusIdx >= kVisibleRows) { scrollOffset = focusIdx - kVisibleRows + 1; } if (scrollOffset > std::max(0, total - kVisibleRows)) { scrollOffset = std::max(0, total - kVisibleRows); } for (int i = 0; i < kVisibleRows; ++i) { int idx = scrollOffset + i; if (idx >= total) { break; } int ry = kListY + (i * kListH) / kVisibleRows; int nextRy = kListY + ((i + 1) * kListH) / kVisibleRows; int rowH = nextRy - ry; bool isSel = (idx == selected); XSetForeground(dpy, gc, isSel ? theme.spec.selBg : ((i % 2 == 0) ? theme.spec.bgAltEven : theme.spec.bgAltOdd)); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH); if (isSel) { XSetForeground(dpy, gc, theme.spec.accent); XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH); } const ContactEntry& c = filtered[idx]; const char* icon = c.isRecent ? kIconClock : kIconUser; XftColor* icCol = isSel ? &colYellow : (c.isRecent ? &colGreen : &colBlue); drawPango(layoutIcon, icCol, 8, ry, rowH, icon); if (c.name != c.number) { drawPango(layoutMain, isSel ? &colWhite : &colLight, 30, ry + 2, 22, c.name, 200); drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 30, ry + 22, 18, c.number, 200); } else { drawPango(layoutMain, isSel ? &colWhite : &colLight, 30, ry + 2, 22, c.number, 200); drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 30, ry + 22, 18, "Recent number", 200); } } if (total > kVisibleRows) { int sbH = std::max(16, (kListH * kVisibleRows) / total); int sbY = kListY + ((kListH - sbH) * scrollOffset) / (total - kVisibleRows); XSetForeground(dpy, gc, theme.spec.divider); XFillRectangle(dpy, pix, gc, kWinWidth - 3, sbY, 3, sbH); } } int sbY = kWinHeight - kSoftbarH; XSetForeground(dpy, gc, theme.spec.bgFooter); XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, kSoftbarH); XSetForeground(dpy, gc, theme.spec.border); XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, 1); std::string leftLabel = number.empty() ? "Call Log" : "+Sign"; std::string centerLabel = (selected < 0 && isUssdCode(number)) ? "SEND" : "CALL"; std::string rightLabel = number.empty() ? "Close" : "Clear"; if (modal == DialerModal::Imei) { XSetForeground(dpy, gc, theme.spec.bgOverlay); XFillRectangle(dpy, pix, gc, 10, 22, kWinWidth - 20, kWinHeight - kSoftbarH - 34); XSetForeground(dpy, gc, theme.spec.accent); XDrawRectangle(dpy, pix, gc, 10, 22, kWinWidth - 21, kWinHeight - kSoftbarH - 35); drawPango(layoutIcon, &colYellow, 18, 28, 24, kIconInfo); drawPango(layoutMain, &colYellow, 38, 28, 24, "Device IMEI (*#06#)", 184); XSetForeground(dpy, gc, theme.spec.divider); XFillRectangle(dpy, pix, gc, 16, 54, kWinWidth - 32, 1); // Slot 1 IMEI card XSetForeground(dpy, gc, theme.spec.bgAltEven); XFillRectangle(dpy, pix, gc, 16, 62, kWinWidth - 32, 54); XSetForeground(dpy, gc, theme.spec.border); XDrawRectangle(dpy, pix, gc, 16, 62, kWinWidth - 33, 53); drawPango(layoutSmall, &colBlue, 22, 65, 18, "IMEI 1 (SIM Slot 1):"); drawPango(layoutMain, &colWhite, 22, 85, 24, deviceImeis.imei1.empty() ? "Unavailable" : deviceImeis.imei1, kWinWidth - 44); // Slot 2 IMEI card XSetForeground(dpy, gc, theme.spec.bgAltEven); XFillRectangle(dpy, pix, gc, 16, 124, kWinWidth - 32, 54); XSetForeground(dpy, gc, theme.spec.border); XDrawRectangle(dpy, pix, gc, 16, 124, kWinWidth - 33, 53); drawPango(layoutSmall, &colGreen, 22, 127, 18, "IMEI 2 (SIM Slot 2):"); drawPango(layoutMain, &colWhite, 22, 147, 24, deviceImeis.imei2.empty() ? "Unavailable" : deviceImeis.imei2, kWinWidth - 44); std::string svLine = "IMEI SV: " + (deviceImeis.imeiSv.empty() ? "--" : deviceImeis.imeiSv); drawPango(layoutSmall, &colMuted, 22, 188, 20, svLine, kWinWidth - 44); leftLabel = ""; centerLabel = "OK"; rightLabel = "Close"; } else if (modal == DialerModal::Ussd) { XSetForeground(dpy, gc, theme.spec.bgOverlay); XFillRectangle(dpy, pix, gc, 8, 14, kWinWidth - 16, kWinHeight - kSoftbarH - 22); XSetForeground(dpy, gc, theme.spec.accent); XDrawRectangle(dpy, pix, gc, 8, 14, kWinWidth - 17, kWinHeight - kSoftbarH - 23); std::string hdr = "USSD (" + ussdCode + ")"; drawPango(layoutMain, &colYellow, 16, 18, 22, hdr, kWinWidth - 32); // Response area XSetForeground(dpy, gc, theme.spec.bgAltEven); XFillRectangle(dpy, pix, gc, 14, 42, kWinWidth - 28, 152); XSetForeground(dpy, gc, theme.spec.border); XDrawRectangle(dpy, pix, gc, 14, 42, kWinWidth - 29, 151); pango_layout_set_width(layoutSmall, (kWinWidth - 40) * PANGO_SCALE); pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR); pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE); pango_layout_set_text(layoutSmall, ussdResponse.c_str(), static_cast(ussdResponse.size())); int lineCount = pango_layout_get_line_count(layoutSmall); int maxVisLines = 7; if (ussdScrollLine > std::max(0, lineCount - maxVisLines)) { ussdScrollLine = std::max(0, lineCount - maxVisLines); } int curY = 46; for (int li = ussdScrollLine; li < lineCount && (li - ussdScrollLine) < maxVisLines; ++li) { PangoLayoutLine* pl = pango_layout_get_line_readonly(layoutSmall, li); if (pl) { PangoRectangle ink{}, logical{}; pango_layout_line_get_pixel_extents(pl, &ink, &logical); pango_xft_render_layout_line( xftDraw, &colWhite, pl, 20 * PANGO_SCALE, (curY - logical.y) * PANGO_SCALE); curY += std::max(18, logical.height + 2); } } pango_layout_set_width(layoutSmall, -1); // Reply / Follow-up USSD input box drawPango(layoutSmall, ussdSessionActive ? &colGreen : &colMuted, 16, 198, 18, ussdSessionActive ? "Session active - Reply:" : "Reply or dial USSD:"); XSetForeground(dpy, gc, theme.spec.bgInputFocus); XFillRectangle(dpy, pix, gc, 14, 216, kWinWidth - 28, 26); XSetForeground(dpy, gc, theme.spec.selBg); XDrawRectangle(dpy, pix, gc, 14, 216, kWinWidth - 29, 25); drawPango(layoutMain, &colWhite, 20, 216, 26, ussdReplyInput + "_", kWinWidth - 40); if (!ussdReplyInput.empty() || ussdSessionActive) { leftLabel = "Send"; centerLabel = "SEND"; rightLabel = "Close"; } else { leftLabel = "Retry"; centerLabel = "OK"; rightLabel = "Close"; } } drawPango(layoutMain, &colLight, 6, sbY + 1, kSoftbarH - 1, leftLabel); int cw = pangoWidth(layoutMain, centerLabel); drawPango(layoutMain, &colGreen, (kWinWidth - cw) / 2, sbY + 1, kSoftbarH - 1, centerLabel); int rw = pangoWidth(layoutMain, rightLabel); drawPango(layoutMain, &colLight, kWinWidth - rw - 6, sbY + 1, kSoftbarH - 1, rightLabel); if (visible) { XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); XFlush(dpy); } }; // Pre-warm Pango/FreeType/Xrender glyph caches into offscreen Pixmap at startup number = "+0123456789*#"; updateFilter(); render(); drawPango(layoutIcon, &colGreen, 0, 0, 20, std::string(kIconPhone) + kIconBackspace + kIconClock + kIconUser); number.clear(); updateFilter(); render(); XSync(dpy, False); auto openImeiModal = [&]() { deviceImeis = queryDeviceImeis(); modal = DialerModal::Imei; number.clear(); selected = allContacts.empty() ? -1 : 0; updateFilter(); render(); }; auto executeUssd = [&](const std::string& codeOrReply, bool isReply) { modal = DialerModal::Ussd; if (!isReply || isUssdCode(codeOrReply)) { ussdCode = codeOrReply; } ussdScrollLine = 0; ussdResponse = "Sending USSD " + codeOrReply + "..."; ussdReplyInput.clear(); render(); ussdResponse = sendModemUssd(codeOrReply, isReply, &ussdSessionActive); render(); }; auto showWindow = [&](const std::string& arg) { closeOtherPlugins("dialer"); modal = DialerModal::Inactive; bool themeChanged = theme.checkAndReload( dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon, layoutNum, win); std::string cleanArg = normalizePhone(arg); if (!visible) { openStartMs = nowMs(); startupIgnoreUntilMs = openStartMs + 70; initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); softkeyPending = 0; softkeyDeadlineMs = 0; zeroHeld = (cleanArg == "0" && isKeycodeDown(dpy, 19)); zeroPressMs = zeroHeld ? openStartMs : 0; visible = true; XMapRaised(dpy, win); if (cleanArg.empty() && !themeChanged) { XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); } for (int i = 0; i < 20; ++i) { if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime) == GrabSuccess) { break; } usleep(2000); } bool reloaded = reloadContactsIfNeeded(); if (cleanArg == "*#06#") { openImeiModal(); } else if (!cleanArg.empty() || reloaded || themeChanged || number != cleanArg) { number = cleanArg; selected = number.empty() ? (allContacts.empty() ? -1 : 0) : -1; updateFilter(); render(); } XFlush(dpy); writeIntFile(activePidPath, static_cast(::getpid())); } else if (!cleanArg.empty()) { if (nowMs() - openStartMs < 40 && number == cleanArg) { return; } number += cleanArg; selected = -1; if (number == "*#06#") { openImeiModal(); } else { updateFilter(); render(); } } }; auto hideWindow = [&](bool drainSoftkeys) { if (!visible) { return; } int xfd = ConnectionNumber(dpy); if (drainSoftkeys) { int64_t drainDeadline = nowMs() + 180; while (nowMs() < drainDeadline && querySoftkeysDown(dpy) != 0) { while (XPending(dpy)) { XEvent ev; XNextEvent(dpy, &ev); } struct pollfd pfd {}; pfd.fd = xfd; pfd.events = POLLIN; ::poll(&pfd, 1, 10); } writeSoftkeySuppress(nowMs() + 300); } XUngrabKeyboard(dpy, CurrentTime); XUnmapWindow(dpy, win); XFlush(dpy); ::unlink(activePidPath.c_str()); visible = false; softkeyPending = 0; softkeyDeadlineMs = 0; zeroHeld = false; if (modal == DialerModal::Ussd && ussdSessionActive) { cancelModemUssd(); ussdSessionActive = false; } if (modal != DialerModal::Inactive || !number.empty() || selected != (allContacts.empty() ? -1 : 0)) { modal = DialerModal::Inactive; number.clear(); selected = allContacts.empty() ? -1 : 0; updateFilter(); render(); XFlush(dpy); } }; if (!isPreload) { showWindow(initArg); } int xfd = ConnectionNumber(dpy); auto selectActiveNumber = [&]() -> std::string { if (selected >= 0 && selected < static_cast(filtered.size())) { return filtered[selected].number; } if (!number.empty()) { return number; } if (!filtered.empty()) { return filtered[0].number; } return ""; }; while (gRunning) { // 1. Process any incoming FIFO commands for (const std::string& cmd : readFifoLines(fifoFd)) { if (cmd == "NOP") { continue; } else if (cmd == "CLOSE") { hideWindow(false); } else if (cmd == "OPEN") { showWindow(""); } else if (cmd.rfind("OPEN ", 0) == 0) { showWindow(cmd.substr(5)); } else if (cmd == "RELOAD_THEME") { theme.load(dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon, layoutNum, win); render(); } } if (visible && zeroHeld) { if (!isKeycodeDown(dpy, 19)) { zeroHeld = false; } else if (nowMs() - zeroPressMs >= 420) { zeroHeld = false; if (!number.empty() && number.back() == '0') { number.back() = '+'; updateFilter(); render(); } } } if (visible && !initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { initialSoftkeysReleased = true; } while (XPending(dpy)) { XEvent ev; XNextEvent(dpy, &ev); if (!visible) { continue; } if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { initialSoftkeysReleased = true; } if (ev.type == Expose) { if (ev.xexpose.count == 0) { render(); } } else if (ev.type == KeyRelease) { KeySym sym = XLookupKeysym(&ev.xkey, 0); if (ev.xkey.keycode == 19 || sym == XK_0) { zeroHeld = false; } if (sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 || sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 || ev.xkey.keycode == 135 || ev.xkey.keycode == 166) { if (querySoftkeysDown(dpy) == 0) { initialSoftkeysReleased = true; } } } else if (ev.type == KeyPress) { KeySym sym = XLookupKeysym(&ev.xkey, 0); bool isLeftSoft = (sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 || ev.xkey.keycode == 135); bool isRightSoft = (sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 || ev.xkey.keycode == 166); if (isLeftSoft || isRightSoft) { if (!initialSoftkeysReleased || nowMs() < startupIgnoreUntilMs) { continue; } int which = (isLeftSoft ? 1 : 0) | (isRightSoft ? 2 : 0); softkeyPending |= which | querySoftkeysDown(dpy); if (softkeyPending == 3) { writeSoftkeySuppress(nowMs() + 500); hideWindow(false); if (!sendPluginCommand("xdrawer", "TOGGLE")) { runAsyncArgv("xdrawer"); } break; } if (softkeyDeadlineMs == 0) { softkeyDeadlineMs = nowMs() + 40; } continue; } char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state); if (modal == DialerModal::Imei) { if (sym == XK_Return || sym == XK_KP_Enter || sym == XK_Escape || sym == XK_BackSpace || sym == XF86XK_Phone || sym == XF86XK_PickupPhone) { modal = DialerModal::Inactive; render(); } continue; } if (modal == DialerModal::Ussd) { if (dialCh != '\0') { if (ussdReplyInput.size() < 32) { ussdReplyInput.push_back(dialCh); render(); } continue; } if (sym == XK_BackSpace || sym == XK_Left || sym == XK_KP_Left) { if (!ussdReplyInput.empty()) { ussdReplyInput.pop_back(); render(); } else if (sym == XK_BackSpace) { if (ussdSessionActive) { cancelModemUssd(); ussdSessionActive = false; } modal = DialerModal::Inactive; render(); } continue; } if (sym == XK_Escape) { if (ussdSessionActive) { cancelModemUssd(); ussdSessionActive = false; } modal = DialerModal::Inactive; render(); continue; } if (sym == XK_Up || sym == XK_KP_Up) { if (ussdScrollLine > 0) { --ussdScrollLine; render(); } continue; } if (sym == XK_Down || sym == XK_KP_Down) { ++ussdScrollLine; render(); continue; } if (sym == XK_Return || sym == XK_KP_Enter || sym == XF86XK_Phone || sym == XF86XK_PickupPhone || sym == XF86XK_Send) { if (!ussdReplyInput.empty()) { if (ussdReplyInput == "*#06#") { openImeiModal(); } else { std::string req = ussdReplyInput; executeUssd(req, !isUssdCode(req)); } } else if (!ussdSessionActive) { modal = DialerModal::Inactive; render(); } continue; } continue; } if (dialCh != '\0') { if (nowMs() - openStartMs < 40 && number.size() == 1 && number[0] == dialCh) { continue; } number.push_back(dialCh); selected = -1; if (dialCh == '0') { zeroHeld = true; zeroPressMs = nowMs(); } else { zeroHeld = false; } if (number == "*#06#") { openImeiModal(); continue; } updateFilter(); render(); continue; } switch (sym) { case XK_Escape: writeSoftkeySuppress(nowMs() + 300); hideWindow(true); break; case XK_BackSpace: case XK_Left: case XK_KP_Left: if (!number.empty()) { number.pop_back(); selected = -1; updateFilter(); render(); } else if (sym == XK_BackSpace) { hideWindow(true); } break; case XK_Right: case XK_KP_Right: if (number.empty() || number[0] != '+') { number = "+" + number; selected = -1; updateFilter(); render(); } break; case XK_Up: case XK_KP_Up: if (!filtered.empty()) { if (selected <= -1) { selected = static_cast(filtered.size()) - 1; } else if (selected == 0 && !number.empty()) { selected = -1; } else if (selected == 0) { selected = static_cast(filtered.size()) - 1; } else { --selected; } render(); } break; case XK_Down: case XK_KP_Down: if (!filtered.empty()) { if (selected < 0) { selected = 0; } else if (selected + 1 < static_cast(filtered.size())) { ++selected; } else { selected = number.empty() ? 0 : -1; } render(); } break; case XK_Return: case XK_KP_Enter: case XF86XK_Phone: case XF86XK_PickupPhone: case XF86XK_Send: { std::string target = selectActiveNumber(); if (!target.empty()) { if (target == "*#06#") { openImeiModal(); } else if (isUssdCode(target)) { number.clear(); selected = allContacts.empty() ? -1 : 0; updateFilter(); executeUssd(target, false); } else { writeSoftkeySuppress(nowMs() + 350); hideWindow(true); placePhoneCall(target); } } break; } default: break; } } } if (visible && softkeyPending != 0 && nowMs() >= softkeyDeadlineMs) { int comb = softkeyPending | querySoftkeysDown(dpy); softkeyPending = 0; softkeyDeadlineMs = 0; if (comb == 3) { writeSoftkeySuppress(nowMs() + 500); hideWindow(false); if (!sendPluginCommand("xdrawer", "TOGGLE")) { runAsyncArgv("xdrawer"); } } else if (comb == 1) { if (modal == DialerModal::Imei) { modal = DialerModal::Inactive; render(); } else if (modal == DialerModal::Ussd) { if (!ussdReplyInput.empty()) { if (ussdReplyInput == "*#06#") { openImeiModal(); } else { std::string req = ussdReplyInput; executeUssd(req, !isUssdCode(req)); } } else if (!ussdCode.empty()) { executeUssd(ussdCode, false); } } else if (number.empty()) { writeSoftkeySuppress(nowMs() + 250); hideWindow(false); if (!sendPluginCommand("calllog", "OPEN")) { runAsyncArgv("xcalllog"); } } else { if (number[0] == '+') { number.erase(0, 1); } else { number = "+" + number; } selected = -1; updateFilter(); render(); } } else if (comb == 2) { if (modal == DialerModal::Imei) { modal = DialerModal::Inactive; render(); } else if (modal == DialerModal::Ussd) { if (ussdSessionActive) { cancelModemUssd(); ussdSessionActive = false; } modal = DialerModal::Inactive; render(); } else if (!number.empty()) { number.pop_back(); selected = -1; updateFilter(); render(); } else { writeSoftkeySuppress(nowMs() + 350); hideWindow(true); } } } int timeoutMs = -1; if (visible) { timeoutMs = 200; if (softkeyDeadlineMs > 0) { timeoutMs = static_cast(std::max(5, softkeyDeadlineMs - nowMs())); } else if (zeroHeld) { timeoutMs = 25; } } struct pollfd pfds[2] {}; pfds[0].fd = xfd; pfds[0].events = POLLIN; pfds[1].fd = fifoFd; pfds[1].events = POLLIN; ::poll(pfds, (fifoFd >= 0) ? 2 : 1, timeoutMs); } hideWindow(false); if (fifoFd >= 0) { ::close(fifoFd); } ::unlink(getPluginFifoPath("dialer").c_str()); ::unlink(daemonPidPath.c_str()); g_object_unref(layoutIcon); g_object_unref(layoutSmall); g_object_unref(layoutMain); g_object_unref(layoutNum); g_object_unref(pangoCtx); XftDrawDestroy(xftDraw); XFreeGC(dpy, gc); XFreePixmap(dpy, pix); XDestroyWindow(dpy, win); XCloseDisplay(dpy); return 0; } int dialer_plugin_init(const DwmHostApi* host) { if (host && host->spawn_plugin) { host->spawn_plugin("dialer", "--preload"); } return 0; } int dialer_on_idle_key(const DwmHostApi* host, KeySym keysym, unsigned int keycode, unsigned int state) { if (!host || !host->is_idle_desktop || !host->is_idle_desktop()) { return 0; } char ch = mapDialKey(keysym, keycode, state); if (ch == '\0') { return 0; } char buf[2] = {ch, '\0'}; if (host->spawn_plugin) { host->spawn_plugin("dialer", buf); } return 1; } } // namespace extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { DWM_PLUGIN_API_VERSION, "dialer", "Keypad-driven dialer with contact & T9 integration (opens on idle 0-9/*/#)", dialer_plugin_init, nullptr, nullptr, dialer_on_idle_key, nullptr, dialer_run_main };