// SPDX-License-Identifier: GPL-2.0-only // CallLog DWM Plugin - Preloaded resident X11 call log viewer & dialer launcher (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 calllog_run_main(int argc, char** argv) { gRunning = 1; bool isPreload = false; for (int i = 1; i < argc; ++i) { if (argv[i] && std::strcmp(argv[i], "--preload") == 0) { isPreload = true; } } if (sendPluginCommand("calllog", isPreload ? "NOP" : "OPEN")) { return 0; } std::string daemonPidPath = getPluginDaemonPidPath("calllog"); std::string activePidPath = getRuntimeFile("xcalllog.pid"); writeIntFile(daemonPidPath, static_cast(::getpid())); int fifoFd = setupPluginDaemonFifo("calllog"); 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[] = "xcalllog"; char resClass[] = "XCallLog"; ch.res_name = resName; ch.res_class = resClass; XSetClassHint(dpy, win, &ch); XStoreName(dpy, win, "Call Log"); 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* 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, nullptr, 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; XftColor& colRed = theme.colRed; ContactBook book; std::vector allLogs; std::vector filtered; FileStamp contactsStamp; FileStamp logStamp; int filterTab = 0; // 0 = All, 1 = Missed, 2 = In, 3 = Out int selected = 0; bool visible = false; int64_t openStartMs = 0; int64_t startupIgnoreUntilMs = 0; bool initialSoftkeysReleased = true; int softkeyPending = 0; int64_t softkeyDeadlineMs = 0; auto applyFilter = [&]() { filtered.clear(); for (const auto& item : allLogs) { if (filterTab == 1 && item.direction != CallDirection::Missed) continue; if (filterTab == 2 && item.direction != CallDirection::Incoming) continue; if (filterTab == 3 && item.direction != CallDirection::Outgoing) continue; filtered.push_back(item); } if (selected >= static_cast(filtered.size())) { selected = std::max(0, static_cast(filtered.size()) - 1); } }; bool logsLoadedOnce = false; auto reloadLogsIfNeeded = [&]() -> bool { bool cChanged = contactsStamp.checkAndUpdate(getContactsFilePath()); bool lChanged = logStamp.checkAndUpdate(getModemLogFilePath()); if (cChanged || lChanged || !logsLoadedOnce) { logsLoadedOnce = true; book.load(); allLogs = loadCallLog(book); applyFilter(); return true; } return false; }; reloadLogsIfNeeded(); applyFilter(); 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 = 34; constexpr int kSoftbarH = 26; constexpr int kListY = kHeaderH; constexpr int kListH = kWinHeight - kHeaderH - kSoftbarH; constexpr int kVisibleRows = 5; const char* kTabLabels[4] = {"All", "Missed", "In", "Out"}; auto render = [&]() { XSetForeground(dpy, gc, theme.spec.bgMain); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); XSetForeground(dpy, gc, theme.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH); for (int t = 0; t < 4; ++t) { int tx = (t * kWinWidth) / 4; int tw = ((t + 1) * kWinWidth) / 4 - tx; bool active = (t == filterTab); if (active) { XSetForeground(dpy, gc, theme.spec.selBg); XFillRectangle(dpy, pix, gc, tx, 0, tw, kHeaderH - 2); XSetForeground(dpy, gc, theme.spec.accent); XFillRectangle(dpy, pix, gc, tx, kHeaderH - 3, tw, 3); } std::string lbl(kTabLabels[t]); int lw = pangoWidth(layoutSmall, lbl); drawPango(layoutSmall, active ? &colWhite : &colMuted, tx + (tw - lw) / 2, 0, kHeaderH - 2, lbl); } XSetForeground(dpy, gc, theme.spec.border); XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1); if (filtered.empty()) { drawPango(layoutIcon, &colMuted, 112, kListY + 60, 24, kIconPhoneLog); drawPango(layoutMain, &colMuted, 52, kListY + 90, 28, "No call history", 160); drawPango(layoutSmall, &colMuted, 24, kListY + 120, 20, "Press 0-9 or Left Softkey to dial", 200); } else { int total = static_cast(filtered.size()); int scrollOffset = 0; if (selected >= kVisibleRows) { scrollOffset = selected - 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 CallLogItem& item = filtered[idx]; const char* dirIcon = kIconInCall; XftColor* dirCol = &colGreen; const char* dirName = "In"; if (item.direction == CallDirection::Outgoing) { dirIcon = kIconOutCall; dirCol = &colBlue; dirName = "Out"; } else if (item.direction == CallDirection::Missed) { dirIcon = kIconMissedCall; dirCol = &colRed; dirName = "Missed"; } drawPango(layoutIcon, dirCol, 8, ry, rowH, dirIcon); std::string dur = formatDuration(item.durationSec); int durW = 0; if (!dur.empty()) { durW = pangoWidth(layoutSmall, dur); drawPango(layoutSmall, isSel ? &colYellow : &colGreen, kWinWidth - durW - 6, ry + 3, 22, dur); } std::string primary = item.contactName.empty() ? item.number : item.contactName; XftColor* priCol = (item.direction == CallDirection::Missed && !isSel) ? &colRed : (isSel ? &colWhite : &colLight); int priMaxW = (durW > 0) ? (kWinWidth - 36 - durW - 6) : 200; drawPango(layoutMain, priCol, 30, ry + 3, 22, primary, priMaxW); int timeW = pangoWidth(layoutSmall, item.displayTime); drawPango(layoutSmall, isSel ? &colWhite : &colMuted, kWinWidth - timeW - 6, ry + 24, 19, item.displayTime); std::string secondary = !item.contactName.empty() ? item.number : std::string(dirName); int secMaxW = std::max(60, kWinWidth - 36 - timeW - 8); drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 30, ry + 24, 19, secondary, secMaxW); } 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); drawPango(layoutMain, &colLight, 6, sbY + 1, kSoftbarH - 1, "Dialer"); std::string centerLabel = "CALL"; int cw = pangoWidth(layoutMain, centerLabel); drawPango(layoutMain, filtered.empty() ? &colMuted : &colGreen, (kWinWidth - cw) / 2, sbY + 1, kSoftbarH - 1, centerLabel); std::string rightLabel = "Close"; 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 drawPango(layoutIcon, &colGreen, 0, 0, 20, std::string(kIconInCall) + kIconOutCall + kIconMissedCall + kIconPhoneLog); render(); XSync(dpy, False); auto showWindow = [&]() { writeIntFile(activePidPath, static_cast(::getpid())); closeOtherPlugins("calllog"); bool themeChanged = theme.checkAndReload( dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon, nullptr, win); if (!visible) { openStartMs = nowMs(); startupIgnoreUntilMs = openStartMs + 90; initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); softkeyPending = 0; softkeyDeadlineMs = 0; visible = true; XMapRaised(dpy, win); if (!themeChanged) { XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); } grabPluginKeyboard(dpy, win); if (reloadLogsIfNeeded() || themeChanged || filterTab != 0 || selected != 0) { filterTab = 0; selected = 0; applyFilter(); render(); } XFlush(dpy); } else { XRaiseWindow(dpy, win); grabPluginKeyboard(dpy, win); if (reloadLogsIfNeeded() || themeChanged) { 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; if (filterTab != 0 || selected != 0) { filterTab = 0; selected = 0; applyFilter(); render(); XFlush(dpy); } }; if (!isPreload) { showWindow(); } int xfd = ConnectionNumber(dpy); while (gRunning) { for (const std::string& cmd : readFifoLines(fifoFd)) { if (cmd == "NOP") { continue; } else if (cmd == "CLOSE") { hideWindow(false); } else if (cmd.rfind("OPEN", 0) == 0) { showWindow(); } else if (cmd == "RELOAD_THEME") { theme.load(dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon, nullptr, win); 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 == VisibilityNotify && ev.xvisibility.state != VisibilityUnobscured) { if (isAnotherPluginActive("calllog")) { hideWindow(false); } else { XRaiseWindow(dpy, win); grabPluginKeyboard(dpy, win); render(); } } else if (ev.type == KeyRelease) { KeySym sym = XLookupKeysym(&ev.xkey, 0); 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 (dialCh != '\0') { std::string arg(1, dialCh); hideWindow(false); if (!sendPluginCommand("dialer", "OPEN " + arg)) { runAsyncArgv("xdialer", arg.c_str()); } break; } switch (sym) { case XK_Escape: case XK_BackSpace: case XK_q: writeSoftkeySuppress(nowMs() + 300); hideWindow(true); break; case XK_Left: case XK_KP_Left: case XK_h: filterTab = (filterTab + 3) % 4; selected = 0; applyFilter(); render(); break; case XK_Right: case XK_KP_Right: case XK_l: filterTab = (filterTab + 1) % 4; selected = 0; applyFilter(); render(); break; case XK_Up: case XK_KP_Up: case XK_k: if (!filtered.empty()) { selected = (selected + static_cast(filtered.size()) - 1) % static_cast(filtered.size()); render(); } break; case XK_Down: case XK_KP_Down: case XK_j: if (!filtered.empty()) { selected = (selected + 1) % static_cast(filtered.size()); render(); } break; case XK_Return: case XK_KP_Enter: case XF86XK_Phone: case XF86XK_PickupPhone: if (nowMs() < startupIgnoreUntilMs) { break; } if (!filtered.empty() && selected >= 0 && selected < static_cast(filtered.size())) { std::string num = filtered[selected].number; writeSoftkeySuppress(nowMs() + 350); hideWindow(true); placePhoneCall(num); } else if (filtered.empty()) { hideWindow(false); if (!sendPluginCommand("dialer", "OPEN")) { runAsyncArgv("xdialer"); } } 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) { std::string arg; if (!filtered.empty() && selected >= 0 && selected < static_cast(filtered.size())) { arg = filtered[selected].number; } hideWindow(false); if (!sendPluginCommand("dialer", arg.empty() ? "OPEN" : ("OPEN " + arg))) { if (arg.empty()) { runAsyncArgv("xdialer"); } else { runAsyncArgv("xdialer", arg.c_str()); } } } else if (comb == 2) { writeSoftkeySuppress(nowMs() + 350); hideWindow(true); } } int timeoutMs = -1; if (visible) { timeoutMs = 200; if (softkeyDeadlineMs > 0) { timeoutMs = static_cast(std::max(5, softkeyDeadlineMs - nowMs())); } } 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("calllog").c_str()); ::unlink(daemonPidPath.c_str()); g_object_unref(layoutIcon); g_object_unref(layoutSmall); g_object_unref(layoutMain); g_object_unref(pangoCtx); XftDrawDestroy(xftDraw); XFreeGC(dpy, gc); XFreePixmap(dpy, pix); XDestroyWindow(dpy, win); XCloseDisplay(dpy); return 0; } int calllog_plugin_init(const DwmHostApi* host) { if (host && host->spawn_plugin) { host->spawn_plugin("calllog", "--preload"); } return 0; } int calllog_on_global_key(const DwmHostApi* host, KeySym keysym, unsigned int keycode, unsigned int state, int is_press) { (void)host; if (!is_press || state != 0) { return 0; } if (keysym == XF86XK_Phone || keysym == XF86XK_PickupPhone || keycode == 177) { if (isCallRingingFast()) { if (!sendPluginCommand("callscreen", "ACCEPT")) { runAsyncArgv("xcallscreen", "accept"); } return 1; } if (isCallActiveOrRingingFast()) { if (!sendPluginCommand("callscreen", "OPEN")) { runAsyncArgv("xcallscreen"); } return 1; } } return 0; } int calllog_on_idle_key(const DwmHostApi* host, KeySym keysym, unsigned int keycode, unsigned int state) { if (!host || !host->is_idle_desktop || !host->is_idle_desktop() || state != 0) { return 0; } if (keysym == XF86XK_Phone || keysym == XF86XK_PickupPhone || keycode == 177) { if (isCallRingingFast()) { if (!sendPluginCommand("callscreen", "ACCEPT")) { runAsyncArgv("xcallscreen", "accept"); } return 1; } if (isCallActiveOrRingingFast()) { if (!sendPluginCommand("callscreen", "OPEN")) { runAsyncArgv("xcallscreen"); } return 1; } if (host->spawn_plugin) { host->spawn_plugin("calllog", nullptr); } return 1; } return 0; } } // namespace extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { DWM_PLUGIN_API_VERSION, "calllog", "Keypad-driven call log viewer & dialer launcher (opens on idle Call key)", calllog_plugin_init, nullptr, nullptr, calllog_on_idle_key, calllog_on_global_key, calllog_run_main };