diff --git a/overlay/rootfs/usr/bin/XDrawer b/overlay/rootfs/usr/bin/XDrawer index 8199a5b..e492465 120000 --- a/overlay/rootfs/usr/bin/XDrawer +++ b/overlay/rootfs/usr/bin/XDrawer @@ -1 +1 @@ -xdrawer \ No newline at end of file +dwm-plugin \ No newline at end of file diff --git a/overlay/rootfs/usr/bin/dwm b/overlay/rootfs/usr/bin/dwm index 8d4ac63..87e21c5 100755 Binary files a/overlay/rootfs/usr/bin/dwm and b/overlay/rootfs/usr/bin/dwm differ diff --git a/overlay/rootfs/usr/bin/dwm-plugin b/overlay/rootfs/usr/bin/dwm-plugin new file mode 100755 index 0000000..87c34bf Binary files /dev/null and b/overlay/rootfs/usr/bin/dwm-plugin differ diff --git a/overlay/rootfs/usr/bin/sxmo_appmenu.sh b/overlay/rootfs/usr/bin/sxmo_appmenu.sh index 2ac1526..d60a1da 100755 --- a/overlay/rootfs/usr/bin/sxmo_appmenu.sh +++ b/overlay/rootfs/usr/bin/sxmo_appmenu.sh @@ -155,6 +155,7 @@ $icon_grd Apps^0^sxmo_appmenu.sh applications $icon_grd Scripts^0^sxmo_appmenu.sh scripts $icon_dir Files^0^sxmo_files.sh $icon_phn Dialer^0^sxmo_modemdial.sh +$icon_phl Call Log^0^xcalllog $icon_msg Texts^0^sxmo_modemtext.sh $icon_usr Contacts^0^sxmo_contactmenu.sh ${_bt_line}$icon_cam Camera^0^bananarchy-photo --menu diff --git a/overlay/rootfs/usr/bin/sxmo_modemdial.sh b/overlay/rootfs/usr/bin/sxmo_modemdial.sh new file mode 100755 index 0000000..57a6e12 --- /dev/null +++ b/overlay/rootfs/usr/bin/sxmo_modemdial.sh @@ -0,0 +1,133 @@ +#!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2022 Sxmo Contributors + +# include common definitions +# shellcheck source=scripts/core/sxmo_common.sh +. sxmo_common.sh + +set -e + +dial_number() { + + # on pinephone if you attempt to make a call while in a call, modem crashes + if sxmo_modemcall.sh list_active_calls | grep -q .; then + sxmo_notify_user.sh "Cannot make call while in a call (for now)." + return 0 + fi + + NUMBER="$1" + + CLEANEDNUMBER="$(pnc find ${DEFAULT_COUNTRY:+-c "$DEFAULT_COUNTRY"} "$1" 2>/dev/null || true)" + if [ -n "$CLEANEDNUMBER" ] && [ "$NUMBER" != "$CLEANEDNUMBER" ]; then + NUMBER="$(cat </dev/null || true + sxmo_log "Starting call with CALLID: $CALLID" + + sxmo_log "Invoking dial hook" + sxmo_hook_dial.sh + + if ! sxmo_modemaudio.sh setup_audio; then + sxmo_notify_user.sh --urgency=critical "We failed to setup call audio" + mmcli -m any --voice-delete-call="$CALLID" + return 1 + fi + + if ! sxmo_modemcall.sh pickup "$CALLID"; then + sxmo_modemaudio.sh reset_audio + mmcli -m any --voice-delete-call="$CALLID" + return 1 + fi + + sxmo_jobs.sh start proximity_lock sxmo_proximitylock.sh + sxmo_hook_statusbar.sh state & + + sxmo_jobs.sh start incall_menu sxmo_modemcall.sh incall_menu +} + +dial_ussd() { + + USSD="$1" + DBUS_MODEM=$(mmcli -J -L | jq -r '."modem-list"[]') + MM_BUS="org.freedesktop.ModemManager1" + USSD_IFACE="org.freedesktop.ModemManager1.Modem.Modem3gpp.Ussd" + + sxmo_log "Dialling USSD: $USSD" + OUTPUT=$(busctl --json=short call "$MM_BUS" "$DBUS_MODEM" "$USSD_IFACE" Initiate "s" "$USSD" | jq -r '.data[]') + + sxmo_terminal.sh sxmo_ussd.sh "$DBUS_MODEM" "Unknown" "$OUTPUT" & +} + +dial_menu() { + if [ -n "${DISPLAY:-}" ] && command -v xdialer >/dev/null 2>&1; then + exec xdialer + fi + + # Initial menu with recently contacted people + NUMBER="$( + grep . < +#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; +} + +bool isCallRinging() { + std::string ringPidFile = getRuntimeFile("sxmo.ring.pid"); + int pid = readIntFile(ringPidFile, -1); + if (pid > 0 && ::kill(pid, 0) == 0) { + return true; + } + return ::system("pidof -s sxmo_RepeatRing.sh >/dev/null 2>&1") == 0; +} + +int calllog_run_main(int, char**) { + gRunning = 1; + int64_t startMs = nowMs(); + + std::string pidPath = getRuntimeFile("xcalllog.pid"); + int existingPid = readIntFile(pidPath, -1); + if (existingPid > 0 && existingPid != static_cast(::getpid())) { + if (::kill(existingPid, 0) == 0) { + return 0; + } + } + writeIntFile(pidPath, static_cast(::getpid())); + + 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) { + ::unlink(pidPath.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"); + + XMapRaised(dpy, win); + XSync(dpy, False); + + for (int i = 0; i < 60; ++i) { + if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime) == + GrabSuccess) { + break; + } + usleep(5000); + } + + 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); + PangoFontDescription* descMain = + pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 14px"); + pango_layout_set_font_description(layoutMain, descMain); + + PangoLayout* layoutSmall = pango_layout_new(pangoCtx); + PangoFontDescription* descSmall = + pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 11px"); + pango_layout_set_font_description(layoutSmall, descSmall); + + PangoLayout* layoutIcon = pango_layout_new(pangoCtx); + PangoFontDescription* descIcon = + pango_font_description_from_string("DejaVuSansM Nerd Font, Fira Code 15px"); + pango_layout_set_font_description(layoutIcon, descIcon); + + auto allocColor = [&](const char* hex) { + XftColor col{}; + XftColorAllocName(dpy, visual, cmap, hex, &col); + return col; + }; + + XftColor colWhite = allocColor("#ffffff"); + XftColor colLight = allocColor("#dddddd"); + XftColor colMuted = allocColor("#999999"); + XftColor colYellow = allocColor("#f4d03f"); + XftColor colGreen = allocColor("#2ecc71"); + XftColor colBlue = allocColor("#5dade2"); + XftColor colRed = allocColor("#e74c3c"); + + ContactBook book; + book.load(); + std::vector allLogs = loadCallLog(book); + std::vector filtered; + + int filterTab = 0; // 0 = All, 1 = Missed, 2 = In, 3 = Out + int selected = 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); + } + }; + + 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; // 236px + constexpr int kVisibleRows = 5; + + const char* kTabLabels[4] = {"All", "Missed", "In", "Out"}; + + auto render = [&]() { + XSetForeground(dpy, gc, 0x222222); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); + + // 1. Top Filter Tabs Bar (y = 0..34) + XSetForeground(dpy, gc, 0x1c1c1c); + 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, 0x005577); + XFillRectangle(dpy, pix, gc, tx, 0, tw, kHeaderH - 2); + XSetForeground(dpy, gc, 0xf4d03f); + 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, 0x444444); + XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1); + + // 2. Call Log List (y = 34..270, 5 visible rows) + 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 ? 0x005577 : ((i % 2 == 0) ? 0x282828 : 0x2e2e2e)); + XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH); + + if (isSel) { + XSetForeground(dpy, gc, 0xf4d03f); + 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, 0x666666); + XFillRectangle(dpy, pix, gc, kWinWidth - 3, sbY, 3, sbH); + } + } + + // 3. Bottom Softkey Bar (y = 270..296) + int sbY = kWinHeight - kSoftbarH; + XSetForeground(dpy, gc, 0x181818); + XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, kSoftbarH); + XSetForeground(dpy, gc, 0x444444); + XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, 1); + + drawPango(layoutSmall, &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(layoutSmall, rightLabel); + drawPango(layoutSmall, &colLight, kWinWidth - rw - 6, sbY + 1, + kSoftbarH - 1, rightLabel); + + XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); + XFlush(dpy); + }; + + render(); + + int64_t startupIgnoreUntilMs = startMs + 140; + bool initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); + int softkeyPending = 0; + int64_t softkeyDeadlineMs = 0; + bool closedByKey = false; + std::string numberToCall; + std::string dialerInitArg; + bool openDialerAfter = false; + bool launchXDrawer = false; + int xfd = ConnectionNumber(dpy); + + while (gRunning) { + if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { + initialSoftkeysReleased = true; + } + + while (XPending(dpy)) { + XEvent ev; + XNextEvent(dpy, &ev); + if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { + initialSoftkeysReleased = true; + } + + if (ev.type == Expose) { + if (ev.xexpose.count == 0) { + render(); + } + } else if (ev.type == VisibilityNotify) { + if (ev.xvisibility.state != VisibilityUnobscured) { + XRaiseWindow(dpy, win); + XFlush(dpy); + } + } 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); + launchXDrawer = true; + closedByKey = true; + gRunning = 0; + break; + } + if (softkeyDeadlineMs == 0) { + softkeyDeadlineMs = nowMs() + 45; + } + continue; + } + + // Pressing 0-9, *, or # in Call Log switches directly to Dialer with that char + char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state); + if (dialCh != '\0') { + dialerInitArg = std::string(1, dialCh); + openDialerAfter = true; + closedByKey = true; + gRunning = 0; + break; + } + + switch (sym) { + case XK_Escape: + case XK_BackSpace: + case XK_q: + writeSoftkeySuppress(nowMs() + 350); + closedByKey = true; + gRunning = 0; + 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())) { + numberToCall = filtered[selected].number; + writeSoftkeySuppress(nowMs() + 400); + closedByKey = true; + gRunning = 0; + } else if (filtered.empty()) { + openDialerAfter = true; + closedByKey = true; + gRunning = 0; + } + break; + default: + break; + } + } + } + + if (!gRunning) { + break; + } + + if (softkeyPending != 0 && nowMs() >= softkeyDeadlineMs) { + int comb = softkeyPending | querySoftkeysDown(dpy); + softkeyPending = 0; + softkeyDeadlineMs = 0; + if (comb == 3) { + writeSoftkeySuppress(nowMs() + 500); + launchXDrawer = true; + closedByKey = true; + gRunning = 0; + break; + } else if (comb == 1) { + // Left Softkey: open Dialer pre-populated with selected number + if (!filtered.empty() && selected >= 0 && + selected < static_cast(filtered.size())) { + dialerInitArg = filtered[selected].number; + } + openDialerAfter = true; + closedByKey = true; + gRunning = 0; + break; + } else if (comb == 2) { + // Right Softkey: close Call Log + writeSoftkeySuppress(nowMs() + 400); + closedByKey = true; + gRunning = 0; + break; + } + } + + int timeoutMs = 200; + if (softkeyDeadlineMs > 0) { + timeoutMs = static_cast(std::max(5, softkeyDeadlineMs - nowMs())); + } + + struct pollfd pfd {}; + pfd.fd = xfd; + pfd.events = POLLIN; + ::poll(&pfd, 1, timeoutMs); + } + + if (closedByKey && !launchXDrawer && !openDialerAfter) { + int64_t drainDeadline = nowMs() + 200; + 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, 15); + } + writeSoftkeySuppress(nowMs() + 350); + } + + XUngrabKeyboard(dpy, CurrentTime); + pango_font_description_free(descIcon); + pango_font_description_free(descSmall); + pango_font_description_free(descMain); + 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); + ::unlink(pidPath.c_str()); + + if (launchXDrawer) { + runAsyncArgv("xdrawer"); + } else if (openDialerAfter) { + if (!dialerInitArg.empty()) { + runAsyncArgv("xdialer", dialerInitArg.c_str()); + } else { + runAsyncArgv("xdialer"); + } + } else if (!numberToCall.empty()) { + placePhoneCall(numberToCall); + } + + return 0; +} + +int calllog_plugin_init(const DwmHostApi*) { + return 0; +} + +int calllog_on_global_key(const DwmHostApi* host, KeySym keysym, + unsigned int keycode, unsigned int state, + int is_press) { + if (!is_press || state != 0) { + return 0; + } + if (keysym == XF86XK_Phone || keysym == XF86XK_PickupPhone || keycode == 177) { + if (isCallRinging()) { + const char* const argv[] = {"sxmo_hook_pickup.sh", nullptr}; + if (host && host->spawn_command) { + host->spawn_command(argv); + } else { + runAsyncArgv("sxmo_hook_pickup.sh"); + } + 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 (isCallRinging()) { + const char* const argv[] = {"sxmo_hook_pickup.sh", nullptr}; + if (host->spawn_command) { + host->spawn_command(argv); + } + 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 +}; diff --git a/src/dwm-plugins/dialer_plugin.cpp b/src/dwm-plugins/dialer_plugin.cpp new file mode 100644 index 0000000..c358d40 --- /dev/null +++ b/src/dwm-plugins/dialer_plugin.cpp @@ -0,0 +1,744 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Dialer DWM Plugin - Keypad-driven 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; +} + +constexpr const char* kQueuePath = "/tmp/sxmo_dialer_queue"; + +void enqueueDialChars(const char* chars) { + if (!chars || !chars[0]) { + return; + } + int fd = ::open(kQueuePath, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644); + if (fd >= 0) { + (void)::write(fd, chars, std::strlen(chars)); + ::close(fd); + } +} + +std::string drainDialQueue() { + int fd = ::open(kQueuePath, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return ""; + } + char buf[128]; + ssize_t n = ::read(fd, buf, sizeof(buf) - 1); + ::close(fd); + ::unlink(kQueuePath); + if (n <= 0) { + return ""; + } + buf[n] = '\0'; + return normalizePhone(buf); +} + +int dialer_run_main(int argc, char** argv) { + gRunning = 1; + int64_t startMs = nowMs(); + + if (argc > 1 && argv[1] && argv[1][0]) { + enqueueDialChars(argv[1]); + } + + std::string pidPath = getRuntimeFile("xdialer.pid"); + int existingPid = readIntFile(pidPath, -1); + if (existingPid > 0 && existingPid != static_cast(::getpid())) { + if (::kill(existingPid, 0) == 0) { + return 0; + } + } + writeIntFile(pidPath, static_cast(::getpid())); + + 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) { + ::unlink(pidPath.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"); + + XMapRaised(dpy, win); + XSync(dpy, False); + + for (int i = 0; i < 60; ++i) { + if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime) == + GrabSuccess) { + break; + } + usleep(5000); + } + + 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); + PangoFontDescription* descNum = + pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font Bold 18px"); + pango_layout_set_font_description(layoutNum, descNum); + + PangoLayout* layoutMain = pango_layout_new(pangoCtx); + PangoFontDescription* descMain = + pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 14px"); + pango_layout_set_font_description(layoutMain, descMain); + + PangoLayout* layoutSmall = pango_layout_new(pangoCtx); + PangoFontDescription* descSmall = + pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 11px"); + pango_layout_set_font_description(layoutSmall, descSmall); + + PangoLayout* layoutIcon = pango_layout_new(pangoCtx); + PangoFontDescription* descIcon = + pango_font_description_from_string("DejaVuSansM Nerd Font, Fira Code 15px"); + pango_layout_set_font_description(layoutIcon, descIcon); + + auto allocColor = [&](const char* hex) { + XftColor col{}; + XftColorAllocName(dpy, visual, cmap, hex, &col); + return col; + }; + + XftColor colWhite = allocColor("#ffffff"); + XftColor colLight = allocColor("#dddddd"); + XftColor colMuted = allocColor("#999999"); + XftColor colYellow = allocColor("#f4d03f"); + XftColor colGreen = allocColor("#2ecc71"); + XftColor colBlue = allocColor("#5dade2"); + + ContactBook book; + book.load(); + std::vector allContacts = book.buildDialerList(); + std::vector filtered; + + std::string number = drainDialQueue(); + int selected = -1; // -1 = top number box focused; 0..N-1 = contact row + + // If launched with '0' from idle desktop and '0' (keycode 19) is still held, + // allow long-pressing that initial '0' to turn into '+'. + bool zeroHeld = (number == "0" && isKeycodeDown(dpy, 19)); + int64_t zeroPressMs = zeroHeld ? startMs : 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; + } + }; + + updateFilter(); + if (!number.empty()) { + 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; // 214px + constexpr int kVisibleRows = 5; + + auto render = [&]() { + XSetForeground(dpy, gc, 0x222222); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); + + // 1. Top Number Display Box (y = 0..56) + bool topSel = (selected < 0); + XSetForeground(dpy, gc, topSel ? 0x1b3b4b : 0x1c1c1c); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH); + if (topSel) { + XSetForeground(dpy, gc, 0xf4d03f); + XFillRectangle(dpy, pix, gc, 0, 0, 4, kHeaderH); + } + XSetForeground(dpy, gc, topSel ? 0x005577 : 0x444444); + 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); + } + + // Subtitle inside header box (y = 33..54) + 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()) { + 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); + + // 2. Contact / Recent Suggestions List (y = 56..270, 5 visible rows) + 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 ? 0x005577 : ((i % 2 == 0) ? 0x282828 : 0x2e2e2e)); + XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH); + + if (isSel) { + XSetForeground(dpy, gc, 0xf4d03f); + 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, 0x666666); + XFillRectangle(dpy, pix, gc, kWinWidth - 3, sbY, 3, sbH); + } + } + + // 3. Bottom Softkey Bar (y = 270..296) + int sbY = kWinHeight - kSoftbarH; + XSetForeground(dpy, gc, 0x181818); + XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, kSoftbarH); + XSetForeground(dpy, gc, 0x444444); + XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, 1); + + std::string leftLabel = number.empty() ? "Call Log" : "+Sign"; + std::string centerLabel = "CALL"; + std::string rightLabel = number.empty() ? "Close" : "Clear"; + + drawPango(layoutSmall, &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(layoutSmall, rightLabel); + drawPango(layoutSmall, &colLight, kWinWidth - rw - 6, sbY + 1, + kSoftbarH - 1, rightLabel); + + XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); + XFlush(dpy); + }; + + render(); + + int64_t startupIgnoreUntilMs = startMs + 90; + bool initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); + int softkeyPending = 0; + int64_t softkeyDeadlineMs = 0; + bool closedByKey = false; + std::string targetToDial; + bool switchToCallLog = false; + bool launchXDrawer = false; + 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) { + std::string extra = drainDialQueue(); + if (!extra.empty()) { + number += extra; + selected = -1; + updateFilter(); + render(); + } + + if (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 (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { + initialSoftkeysReleased = true; + } + + while (XPending(dpy)) { + XEvent ev; + XNextEvent(dpy, &ev); + if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { + initialSoftkeysReleased = true; + } + + if (ev.type == Expose) { + if (ev.xexpose.count == 0) { + render(); + } + } else if (ev.type == VisibilityNotify) { + if (ev.xvisibility.state != VisibilityUnobscured) { + XRaiseWindow(dpy, win); + XFlush(dpy); + } + } 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); + launchXDrawer = true; + closedByKey = true; + gRunning = 0; + break; + } + if (softkeyDeadlineMs == 0) { + softkeyDeadlineMs = nowMs() + 45; + } + continue; + } + + char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state); + if (dialCh != '\0') { + if (nowMs() - startMs < 45 && number.size() == 1 && + number[0] == dialCh) { + // Ignore duplicate event from the initial key press that opened dialer + continue; + } + number.push_back(dialCh); + selected = -1; + if (dialCh == '0') { + zeroHeld = true; + zeroPressMs = nowMs(); + } else { + zeroHeld = false; + } + updateFilter(); + render(); + continue; + } + + switch (sym) { + case XK_Escape: + writeSoftkeySuppress(nowMs() + 350); + closedByKey = true; + gRunning = 0; + 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) { + closedByKey = true; + gRunning = 0; + } + 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: { + std::string target = selectActiveNumber(); + if (!target.empty()) { + targetToDial = target; + writeSoftkeySuppress(nowMs() + 400); + closedByKey = true; + gRunning = 0; + } + break; + } + default: + break; + } + } + } + + if (!gRunning) { + break; + } + + if (softkeyPending != 0 && nowMs() >= softkeyDeadlineMs) { + int comb = softkeyPending | querySoftkeysDown(dpy); + softkeyPending = 0; + softkeyDeadlineMs = 0; + if (comb == 3) { + writeSoftkeySuppress(nowMs() + 500); + launchXDrawer = true; + closedByKey = true; + gRunning = 0; + break; + } else if (comb == 1) { + // Left Softkey: if empty, switch to Call Log; if non-empty, toggle leading '+' + if (number.empty()) { + writeSoftkeySuppress(nowMs() + 350); + switchToCallLog = true; + closedByKey = true; + gRunning = 0; + break; + } else { + if (number[0] == '+') { + number.erase(0, 1); + } else { + number = "+" + number; + } + selected = -1; + updateFilter(); + render(); + } + } else if (comb == 2) { + // Right Softkey: delete last character if non-empty, else close Dialer + if (!number.empty()) { + number.pop_back(); + selected = -1; + updateFilter(); + render(); + } else { + writeSoftkeySuppress(nowMs() + 400); + closedByKey = true; + gRunning = 0; + break; + } + } + } + + int timeoutMs = 200; + if (softkeyDeadlineMs > 0) { + timeoutMs = static_cast(std::max(5, softkeyDeadlineMs - nowMs())); + } else if (zeroHeld) { + timeoutMs = 25; + } + + struct pollfd pfd {}; + pfd.fd = xfd; + pfd.events = POLLIN; + ::poll(&pfd, 1, timeoutMs); + } + + if (closedByKey && !launchXDrawer) { + int64_t drainDeadline = nowMs() + 200; + 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, 15); + } + writeSoftkeySuppress(nowMs() + 350); + } + + XUngrabKeyboard(dpy, CurrentTime); + pango_font_description_free(descIcon); + pango_font_description_free(descSmall); + pango_font_description_free(descMain); + pango_font_description_free(descNum); + 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); + ::unlink(pidPath.c_str()); + + if (launchXDrawer) { + runAsyncArgv("xdrawer"); + } else if (switchToCallLog) { + runAsyncArgv("xcalllog"); + } else if (!targetToDial.empty()) { + placePhoneCall(targetToDial); + } + + return 0; +} + +int64_t gLastSpawnMs = 0; + +int dialer_plugin_init(const DwmHostApi*) { + 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'}; + enqueueDialChars(buf); + + int64_t now = nowMs(); + std::string pidPath = getRuntimeFile("xdialer.pid"); + int existingPid = readIntFile(pidPath, -1); + bool alreadyRunning = (existingPid > 0 && ::kill(existingPid, 0) == 0); + + if (!alreadyRunning && (now - gLastSpawnMs) > 250 && host->spawn_plugin) { + gLastSpawnMs = now; + host->spawn_plugin("dialer", nullptr); + } + 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 +}; diff --git a/src/dwm-plugins/dwm_plugin_runner.c b/src/dwm-plugins/dwm_plugin_runner.c new file mode 100644 index 0000000..6f32eb0 --- /dev/null +++ b/src/dwm-plugins/dwm_plugin_runner.c @@ -0,0 +1,116 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../sxmo-dwm/dwm_plugin.h" + +static const char *const kPluginDirs[] = { + "/home/user/.local/lib/dwm/plugins", + "/usr/lib/dwm/plugins", + NULL +}; + +static const char *resolve_plugin_name_from_argv0(const char *argv0) { + const char *base = strrchr(argv0, '/'); + base = base ? (base + 1) : argv0; + if (strcasecmp(base, "xdrawer") == 0) { + return "xdrawer"; + } + if (strcasecmp(base, "xdialer") == 0 || strcasecmp(base, "dialer") == 0) { + return "dialer"; + } + if (strcasecmp(base, "xcalllog") == 0 || strcasecmp(base, "calllog") == 0) { + return "calllog"; + } + return NULL; +} + +static int list_plugins(void) { + for (int i = 0; kPluginDirs[i]; ++i) { + DIR *dir = opendir(kPluginDirs[i]); + if (!dir) { + continue; + } + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + size_t len = strlen(ent->d_name); + if (len <= 3 || strcmp(ent->d_name + len - 3, ".so") != 0) { + continue; + } + char path[512]; + snprintf(path, sizeof(path), "%s/%s", kPluginDirs[i], ent->d_name); + void *h = dlopen(path, RTLD_NOW | RTLD_LOCAL); + if (!h) { + continue; + } + const DwmPlugin *plug = (const DwmPlugin *)dlsym(h, DWM_PLUGIN_ENTRY_SYMBOL); + if (plug && plug->api_version == DWM_PLUGIN_API_VERSION && plug->name) { + printf("%-12s %s (%s)\n", + plug->name, + plug->description ? plug->description : "", + path); + } + dlclose(h); + } + closedir(dir); + } + return 0; +} + +int main(int argc, char **argv) { + const char *plugin_name = resolve_plugin_name_from_argv0(argv[0]); + int sub_argc = argc; + char **sub_argv = argv; + + if (!plugin_name) { + if (argc < 2 || strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) { + fprintf(stderr, "Usage: %s [args...]\n", argv[0]); + return 1; + } + if (strcmp(argv[1], "--list") == 0 || strcmp(argv[1], "-l") == 0) { + return list_plugins(); + } + plugin_name = argv[1]; + sub_argc = argc - 1; + sub_argv = argv + 1; + } + + const char *env_dir = getenv("DWM_PLUGIN_PATH"); + char path[512]; + void *handle = NULL; + + if (env_dir && env_dir[0]) { + snprintf(path, sizeof(path), "%s/%s.so", env_dir, plugin_name); + if (access(path, R_OK) == 0) { + handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); + } + } + + for (int i = 0; !handle && kPluginDirs[i]; ++i) { + snprintf(path, sizeof(path), "%s/%s.so", kPluginDirs[i], plugin_name); + if (access(path, R_OK) == 0) { + handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); + } + } + + if (!handle) { + fprintf(stderr, "dwm-plugin: failed to load plugin '%s': %s\n", + plugin_name, dlerror()); + return 1; + } + + const DwmPlugin *plug = (const DwmPlugin *)dlsym(handle, DWM_PLUGIN_ENTRY_SYMBOL); + if (!plug || plug->api_version != DWM_PLUGIN_API_VERSION || !plug->run_main) { + fprintf(stderr, "dwm-plugin: invalid plugin '%s'\n", plugin_name); + dlclose(handle); + return 1; + } + + int rc = plug->run_main(sub_argc, sub_argv); + dlclose(handle); + return rc; +} diff --git a/src/dwm-plugins/phone_data.h b/src/dwm-plugins/phone_data.h new file mode 100644 index 0000000..8606545 --- /dev/null +++ b/src/dwm-plugins/phone_data.h @@ -0,0 +1,625 @@ +#ifndef DWM_PLUGINS_PHONE_DATA_H +#define DWM_PLUGINS_PHONE_DATA_H + +#include +#include +#include +#ifndef _EVDEVK +#define _EVDEVK(_v) (0x10081000 + (_v)) +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bananarchy { + +constexpr int kScreenWidth = 240; +constexpr int kScreenHeight = 320; +constexpr int kTopBarHeight = 24; +constexpr int kWinX = 0; +constexpr int kWinY = kTopBarHeight; +constexpr int kWinWidth = kScreenWidth; +constexpr int kWinHeight = kScreenHeight - kTopBarHeight; // 296px flush below 24px dwm bar + +// Nerd Font PUA icons matching sxmo_hook_icons.sh & dwm ("Fira Code, DejaVuSansM Nerd Font") +constexpr const char* kIconPhone = "\xef\x82\x95"; // U+F095 phone handset +constexpr const char* kIconPhoneLog = "\xef\x82\x98"; // U+F098 phone square +constexpr const char* kIconUser = "\xef\x80\x87"; // U+F007 user contact +constexpr const char* kIconClock = "\xef\x80\x97"; // U+F017 recent clock +constexpr const char* kIconOutCall = "\xef\x85\xb8"; // U+F178 outgoing arrow +constexpr const char* kIconInCall = "\xef\x85\xb5"; // U+F175 incoming arrow +constexpr const char* kIconMissedCall = "\xef\x81\xb1"; // U+F071 missed/alert +constexpr const char* kIconBackspace = "\xef\x86\x90"; // U+F190 left circle arrow + +inline int64_t nowMs() { + struct timespec ts {}; + clock_gettime(CLOCK_MONOTONIC, &ts); + return static_cast(ts.tv_sec) * 1000 + ts.tv_nsec / 1000000; +} + +inline std::string getRuntimeFile(const char* name) { + const char* rdir = std::getenv("XDG_RUNTIME_DIR"); + std::string base = (rdir && rdir[0]) ? rdir : "/run/user/10000"; + if (::access(base.c_str(), W_OK) != 0) { + base = "/tmp"; + } + return base + "/" + name; +} + +inline int readIntFile(const std::string& path, int fallback = -1) { + int fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return fallback; + } + char buf[64]; + ssize_t n = ::read(fd, buf, sizeof(buf) - 1); + ::close(fd); + if (n <= 0) { + return fallback; + } + buf[n] = '\0'; + return std::atoi(buf); +} + +inline bool writeIntFile(const std::string& path, int value) { + int fd = ::open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, 0644); + if (fd < 0) { + return false; + } + char buf[32]; + int len = std::snprintf(buf, sizeof(buf), "%d\n", value); + ssize_t w = ::write(fd, buf, len); + ::close(fd); + return w == len; +} + +inline void writeSoftkeySuppress(int64_t untilMs) { + int fd = ::open("/tmp/sxmo_softkey_suppress", + O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + if (fd >= 0) { + char buf[32]; + int len = std::snprintf(buf, sizeof(buf), "%lld\n", + static_cast(untilMs)); + (void)::write(fd, buf, len); + ::close(fd); + } +} + +inline int querySoftkeysDown(Display* d) { + char km[32]; + if (!XQueryKeymap(d, km)) { + return 0; + } + int mask = 0; + if (km[135 / 8] & (1 << (135 % 8))) { + mask |= 1; + } + if (km[166 / 8] & (1 << (166 % 8))) { + mask |= 2; + } + return mask; +} + +inline bool isKeycodeDown(Display* d, int keycode) { + if (keycode <= 0 || keycode >= 256) { + return false; + } + char km[32]; + if (!XQueryKeymap(d, km)) { + return false; + } + return (km[keycode / 8] & (1 << (keycode % 8))) != 0; +} + +inline void runAsyncCmd(const std::string& cmd) { + pid_t pid = ::fork(); + if (pid == 0) { + ::setsid(); + int nullfd = ::open("/dev/null", O_RDWR); + if (nullfd >= 0) { + ::dup2(nullfd, STDIN_FILENO); + ::dup2(nullfd, STDOUT_FILENO); + ::dup2(nullfd, STDERR_FILENO); + if (nullfd > 2) { + ::close(nullfd); + } + } + execl("/bin/sh", "sh", "-c", cmd.c_str(), static_cast(nullptr)); + _exit(0); + } +} + +inline void runAsyncArgv(const char* prog, const char* arg1 = nullptr) { + pid_t pid = ::fork(); + if (pid == 0) { + ::setsid(); + int nullfd = ::open("/dev/null", O_RDWR); + if (nullfd >= 0) { + ::dup2(nullfd, STDIN_FILENO); + ::dup2(nullfd, STDOUT_FILENO); + ::dup2(nullfd, STDERR_FILENO); + if (nullfd > 2) { + ::close(nullfd); + } + } + if (arg1) { + execlp(prog, prog, arg1, static_cast(nullptr)); + std::string localBin = std::string("/home/user/.local/bin/") + prog; + execl(localBin.c_str(), prog, arg1, static_cast(nullptr)); + std::string usrBin = std::string("/usr/bin/") + prog; + execl(usrBin.c_str(), prog, arg1, static_cast(nullptr)); + } else { + execlp(prog, prog, static_cast(nullptr)); + std::string localBin = std::string("/home/user/.local/bin/") + prog; + execl(localBin.c_str(), prog, static_cast(nullptr)); + std::string usrBin = std::string("/usr/bin/") + prog; + execl(usrBin.c_str(), prog, static_cast(nullptr)); + } + _exit(0); + } +} + +inline char mapDialKey(KeySym sym, unsigned int keycode, unsigned int state) { + if (state & (ControlMask | Mod1Mask | Mod4Mask)) { + return '\0'; + } + // Keypad star (*) + if (keycode == 63 || keycode == 530 || sym == XK_asterisk || + sym == XK_KP_Multiply || static_cast(sym) == 0x1008ffaa) { + return '*'; + } + // Keypad pound (#) + if (keycode == 106 || keycode == 531 || sym == XK_numbersign || + sym == XK_KP_Divide || static_cast(sym) == 0x1008ffab) { + return '#'; + } + if (sym == XK_plus || sym == XK_KP_Add) { + return '+'; + } + if (sym >= XK_0 && sym <= XK_9) { + return static_cast('0' + (sym - XK_0)); + } + if (sym >= XK_KP_0 && sym <= XK_KP_9) { + return static_cast('0' + (sym - XK_KP_0)); + } + if (keycode >= 10 && keycode <= 18) { + return static_cast('1' + (keycode - 10)); + } + if (keycode == 19) { + return '0'; + } + return '\0'; +} + +inline std::string trimStr(const std::string& s) { + size_t b = 0; + while (b < s.size() && std::isspace(static_cast(s[b]))) { + ++b; + } + size_t e = s.size(); + while (e > b && std::isspace(static_cast(s[e - 1]))) { + --e; + } + return s.substr(b, e - b); +} + +inline std::string normalizePhone(const std::string& raw) { + std::string out; + out.reserve(raw.size()); + for (char c : raw) { + if (std::isdigit(static_cast(c)) || c == '+' || c == '*' || + c == '#') { + out.push_back(c); + } + } + return out; +} + +inline std::string phoneSuffixKey(const std::string& norm) { + std::string digits; + for (char c : norm) { + if (std::isdigit(static_cast(c))) { + digits.push_back(c); + } + } + if (digits.size() >= 9) { + return digits.substr(digits.size() - 9); + } + return norm; +} + +inline std::string toLowerStr(const std::string& s) { + std::string out = s; + for (char& c : out) { + c = static_cast(std::tolower(static_cast(c))); + } + return out; +} + +inline std::string toT9Digits(const std::string& name) { + std::string out; + out.reserve(name.size()); + for (unsigned char uc : name) { + char c = static_cast(std::tolower(uc)); + if (c >= 'a' && c <= 'c') out.push_back('2'); + else if (c >= 'd' && c <= 'f') out.push_back('3'); + else if (c >= 'g' && c <= 'i') out.push_back('4'); + else if (c >= 'j' && c <= 'l') out.push_back('5'); + else if (c >= 'm' && c <= 'o') out.push_back('6'); + else if (c >= 'p' && c <= 's') out.push_back('7'); + else if (c >= 't' && c <= 'v') out.push_back('8'); + else if (c >= 'w' && c <= 'z') out.push_back('9'); + else if (c == ' ') out.push_back('0'); + else if (std::isdigit(uc)) out.push_back(c); + } + return out; +} + +inline std::string getContactsFilePath() { + const char* env = std::getenv("SXMO_CONTACTFILE"); + if (env && env[0]) { + return env; + } + const char* xdg = std::getenv("XDG_CONFIG_HOME"); + if (xdg && xdg[0]) { + return std::string(xdg) + "/sxmo/contacts.tsv"; + } + const char* home = std::getenv("HOME"); + return std::string(home && home[0] ? home : "/home/user") + + "/.config/sxmo/contacts.tsv"; +} + +inline std::string getModemLogFilePath() { + const char* env = std::getenv("SXMO_LOGDIR"); + if (env && env[0]) { + return std::string(env) + "/modemlog.tsv"; + } + const char* xdg = std::getenv("XDG_DATA_HOME"); + if (xdg && xdg[0]) { + return std::string(xdg) + "/sxmo/modem/modemlog.tsv"; + } + const char* home = std::getenv("HOME"); + return std::string(home && home[0] ? home : "/home/user") + + "/.local/share/sxmo/modem/modemlog.tsv"; +} + +struct ContactEntry { + std::string name; + std::string number; + std::string normNumber; + std::string t9Name; + std::string lowerName; + bool isRecent = false; +}; + +class ContactBook { + public: + void load() { + saved_.clear(); + exactMap_.clear(); + suffixMap_.clear(); + + std::ifstream cf(getContactsFilePath()); + std::string line; + while (std::getline(cf, line)) { + if (line.empty()) { + continue; + } + size_t tab = line.find('\t'); + if (tab == std::string::npos) { + continue; + } + std::string num = trimStr(line.substr(0, tab)); + std::string name = trimStr(line.substr(tab + 1)); + std::string norm = normalizePhone(num); + if (norm.empty() || name.empty()) { + continue; + } + ContactEntry e; + e.name = name; + e.number = num; + e.normNumber = norm; + e.t9Name = toT9Digits(name); + e.lowerName = toLowerStr(name); + e.isRecent = false; + saved_.push_back(e); + exactMap_[norm] = name; + std::string sfx = phoneSuffixKey(norm); + if (!sfx.empty()) { + suffixMap_[sfx] = name; + } + } + + std::sort(saved_.begin(), saved_.end(), + [](const ContactEntry& a, const ContactEntry& b) { + return a.lowerName < b.lowerName; + }); + } + + std::string resolveName(const std::string& rawNumber) const { + std::string norm = normalizePhone(rawNumber); + if (norm.empty()) { + return ""; + } + auto it = exactMap_.find(norm); + if (it != exactMap_.end()) { + return it->second; + } + std::string sfx = phoneSuffixKey(norm); + if (!sfx.empty()) { + auto sit = suffixMap_.find(sfx); + if (sit != suffixMap_.end()) { + return sit->second; + } + } + return ""; + } + + std::vector buildDialerList() const { + std::vector out; + std::unordered_set seenKey; + + // 1. Recent unique numbers from modemlog.tsv in reverse chronological order + std::ifstream lf(getModemLogFilePath()); + std::vector logLines; + std::string line; + while (std::getline(lf, line)) { + if (!line.empty()) { + logLines.push_back(line); + } + } + for (auto it = logLines.rbegin(); it != logLines.rend(); ++it) { + size_t t1 = it->find('\t'); + if (t1 == std::string::npos) continue; + size_t t2 = it->find('\t', t1 + 1); + if (t2 == std::string::npos) continue; + size_t t3 = it->find('\t', t2 + 1); + std::string num = trimStr( + t3 == std::string::npos ? it->substr(t2 + 1) : it->substr(t2 + 1, t3 - t2 - 1)); + if (num.empty() || num == "NULL" || num.find('+', 1) != std::string::npos) { + continue; + } + std::string norm = normalizePhone(num); + if (norm.empty()) continue; + std::string key = phoneSuffixKey(norm); + if (seenKey.count(key) || seenKey.count(norm)) { + continue; + } + seenKey.insert(key); + seenKey.insert(norm); + + ContactEntry e; + std::string cname = resolveName(num); + e.name = cname.empty() ? num : cname; + e.number = num; + e.normNumber = norm; + e.t9Name = toT9Digits(e.name); + e.lowerName = toLowerStr(e.name); + e.isRecent = true; + out.push_back(e); + } + + // 2. All saved contacts (skipping duplicates already added from recents) + for (const auto& c : saved_) { + std::string key = phoneSuffixKey(c.normNumber); + if (seenKey.count(key) || seenKey.count(c.normNumber)) { + continue; + } + seenKey.insert(key); + seenKey.insert(c.normNumber); + out.push_back(c); + } + + return out; + } + + private: + std::vector saved_; + std::unordered_map exactMap_; + std::unordered_map suffixMap_; +}; + +enum class CallDirection { + Incoming = 0, + Outgoing = 1, + Missed = 2, +}; + +struct CallLogItem { + CallDirection direction; + std::string number; + std::string contactName; + std::string displayTime; + int durationSec = 0; +}; + +inline time_t parseIsoTime(const std::string& iso) { + if (iso.size() < 19) { + return 0; + } + struct tm tmVal {}; + tmVal.tm_year = std::atoi(iso.substr(0, 4).c_str()) - 1900; + tmVal.tm_mon = std::atoi(iso.substr(5, 2).c_str()) - 1; + tmVal.tm_mday = std::atoi(iso.substr(8, 2).c_str()); + tmVal.tm_hour = std::atoi(iso.substr(11, 2).c_str()); + tmVal.tm_min = std::atoi(iso.substr(14, 2).c_str()); + tmVal.tm_sec = std::atoi(iso.substr(17, 2).c_str()); + tmVal.tm_isdst = -1; + return mktime(&tmVal); +} + +inline std::string formatShortTime(const std::string& iso) { + if (iso.size() < 16) { + return iso; + } + // Convert "YYYY-MM-DDTHH:MM:SS+0300" -> "MM-DD HH:MM" + return iso.substr(5, 5) + " " + iso.substr(11, 5); +} + +inline std::string formatDuration(int sec) { + if (sec <= 0) { + return ""; + } + char buf[32]; + if (sec >= 3600) { + std::snprintf(buf, sizeof(buf), "%dh%02dm", sec / 3600, (sec % 3600) / 60); + } else if (sec >= 60) { + std::snprintf(buf, sizeof(buf), "%dm%02ds", sec / 60, sec % 60); + } else { + std::snprintf(buf, sizeof(buf), "%ds", sec); + } + return buf; +} + +inline std::vector loadCallLog(const ContactBook& book) { + std::ifstream lf(getModemLogFilePath()); + std::string line; + + struct ActiveSession { + bool active = false; + CallDirection dir = CallDirection::Incoming; + std::string number; + std::string startTimeStr; + time_t startEpoch = 0; + time_t answerEpoch = 0; + bool answered = false; + }; + + std::vector history; + ActiveSession cur; + + auto flushSession = [&]() { + if (!cur.active || cur.number.empty()) { + cur = ActiveSession{}; + return; + } + CallLogItem item; + item.direction = cur.dir; + item.number = cur.number; + item.contactName = book.resolveName(cur.number); + item.displayTime = formatShortTime(cur.startTimeStr); + item.durationSec = 0; + history.push_back(item); + cur = ActiveSession{}; + }; + + while (std::getline(lf, line)) { + if (line.empty()) continue; + size_t t1 = line.find('\t'); + if (t1 == std::string::npos) continue; + size_t t2 = line.find('\t', t1 + 1); + if (t2 == std::string::npos) continue; + size_t t3 = line.find('\t', t2 + 1); + + std::string ts = trimStr(line.substr(0, t1)); + std::string evt = trimStr(line.substr(t1 + 1, t2 - t1 - 1)); + std::string num = trimStr( + t3 == std::string::npos ? line.substr(t2 + 1) : line.substr(t2 + 1, t3 - t2 - 1)); + + if (evt.rfind("call_", 0) != 0 && evt != "ring_muted") { + continue; + } + if (num.empty() || num == "NULL") { + continue; + } + + time_t epoch = parseIsoTime(ts); + + if (evt == "call_ring") { + flushSession(); + cur.active = true; + cur.dir = CallDirection::Missed; + cur.number = num; + cur.startTimeStr = ts; + cur.startEpoch = epoch; + cur.answerEpoch = 0; + cur.answered = false; + } else if (evt == "call_start") { + flushSession(); + cur.active = true; + cur.dir = CallDirection::Outgoing; + cur.number = num; + cur.startTimeStr = ts; + cur.startEpoch = epoch; + cur.answerEpoch = epoch; + cur.answered = true; + } else if (evt == "call_pickup") { + if (cur.active && normalizePhone(cur.number) == normalizePhone(num)) { + cur.dir = CallDirection::Incoming; + cur.answered = true; + cur.answerEpoch = epoch; + } else { + flushSession(); + cur.active = true; + cur.dir = CallDirection::Incoming; + cur.number = num; + cur.startTimeStr = ts; + cur.startEpoch = epoch; + cur.answerEpoch = epoch; + cur.answered = true; + } + } else if (evt == "call_missed") { + if (cur.active && normalizePhone(cur.number) == normalizePhone(num)) { + cur.dir = CallDirection::Missed; + flushSession(); + } else { + flushSession(); + CallLogItem item; + item.direction = CallDirection::Missed; + item.number = num; + item.contactName = book.resolveName(num); + item.displayTime = formatShortTime(ts); + item.durationSec = 0; + history.push_back(item); + } + } else if (evt == "call_hangup" || evt == "call_finished" || + evt == "call_discard") { + if (cur.active && normalizePhone(cur.number) == normalizePhone(num)) { + if (evt == "call_discard" && !cur.answered && + cur.dir != CallDirection::Outgoing) { + cur.dir = CallDirection::Missed; + } + CallLogItem item; + item.direction = cur.dir; + item.number = cur.number; + item.contactName = book.resolveName(cur.number); + item.displayTime = formatShortTime(cur.startTimeStr); + if (cur.answered && cur.answerEpoch > 0 && epoch >= cur.answerEpoch) { + item.durationSec = static_cast(epoch - cur.answerEpoch); + } + history.push_back(item); + cur = ActiveSession{}; + } + } + } + flushSession(); + + std::reverse(history.begin(), history.end()); + return history; +} + +inline void placePhoneCall(const std::string& rawNumber) { + std::string num = normalizePhone(rawNumber); + if (num.empty()) { + return; + } + runAsyncArgv("sxmo_modemdial.sh", num.c_str()); +} + +} // namespace bananarchy + +#endif // DWM_PLUGINS_PHONE_DATA_H diff --git a/src/sxmo-dwm/dwm.c b/src/sxmo-dwm/dwm.c index 9f73e7e..76393fd 100644 --- a/src/sxmo-dwm/dwm.c +++ b/src/sxmo-dwm/dwm.c @@ -271,6 +271,8 @@ static void shiftview(const Arg *arg); static void pushup(const Arg *arg); static void pushdown(const Arg *arg); static void readxresources(); +static void load_plugins(void); +static void unload_plugins(void); static pid_t getparentprocess(pid_t p); static int isdescprocess(pid_t p, pid_t c); @@ -698,6 +700,7 @@ cleanup(void) drw_cur_free(drw, cursor[i]); for (i = 0; i < LENGTH(colors); i++) free(scheme[i]); + unload_plugins(); XDestroyWindow(dpy, wmcheckwin); drw_free(drw); XSync(dpy, False); @@ -1532,6 +1535,191 @@ is_right_softkey(KeySym ks, unsigned int keycode) return ks == XF86XK_Back || ks == XF86XK_KbdLcdMenu2 || keycode == 166; } +#include +#include +#include "dwm_plugin.h" + +#define MAX_DWM_PLUGINS 16 + +typedef struct { + void *handle; + const DwmPlugin *plugin; +} LoadedDwmPlugin; + +static LoadedDwmPlugin loaded_plugins[MAX_DWM_PLUGINS]; +static int num_loaded_plugins = 0; + +static int +host_is_idle_desktop(void) +{ + return (selmon && selmon->sel == NULL && !is_phone_locked()) ? 1 : 0; +} + +static int +host_is_phone_locked(void) +{ + return is_phone_locked(); +} + +static void +host_spawn_command(const char *const *argv) +{ + if (!argv || !argv[0]) + return; + Arg a = { .v = (const void *)argv }; + XUngrabKeyboard(dpy, CurrentTime); + XFlush(dpy); + spawn(&a); +} + +static void +host_spawn_plugin(const char *plugin_name, const char *arg) +{ + if (!plugin_name || !plugin_name[0]) + return; + const char *cmd[4]; + cmd[0] = "dwm-plugin"; + cmd[1] = plugin_name; + cmd[2] = (arg && arg[0]) ? arg : NULL; + cmd[3] = NULL; + Arg a = { .v = cmd }; + XUngrabKeyboard(dpy, CurrentTime); + XFlush(dpy); + spawn(&a); +} + +static DwmHostApi dwm_host_api = { + .api_version = DWM_PLUGIN_API_VERSION, + .dpy = NULL, + .root = 0, + .screen = 0, + .sw = 0, + .sh = 0, + .bh = 0, + .is_idle_desktop = host_is_idle_desktop, + .is_phone_locked = host_is_phone_locked, + .spawn_command = host_spawn_command, + .spawn_plugin = host_spawn_plugin, +}; + +static int +is_plugin_loaded(const char *name) +{ + int i; + for (i = 0; i < num_loaded_plugins; i++) { + if (loaded_plugins[i].plugin && loaded_plugins[i].plugin->name && + strcmp(loaded_plugins[i].plugin->name, name) == 0) + return 1; + } + return 0; +} + +static void +load_plugins_from_dir(const char *dirpath) +{ + DIR *dir; + struct dirent *ent; + char path[512]; + + if (!dirpath || !dirpath[0]) + return; + dir = opendir(dirpath); + if (!dir) + return; + while ((ent = readdir(dir)) != NULL && num_loaded_plugins < MAX_DWM_PLUGINS) { + size_t len = strlen(ent->d_name); + if (len <= 3 || strcmp(ent->d_name + len - 3, ".so") != 0) + continue; + snprintf(path, sizeof(path), "%s/%s", dirpath, ent->d_name); + void *h = dlopen(path, RTLD_NOW | RTLD_LOCAL); + if (!h) + continue; + const DwmPlugin *plug = (const DwmPlugin *)dlsym(h, DWM_PLUGIN_ENTRY_SYMBOL); + if (!plug || plug->api_version != DWM_PLUGIN_API_VERSION || + !plug->name || is_plugin_loaded(plug->name)) { + dlclose(h); + continue; + } + if (plug->init && plug->init(&dwm_host_api) != 0) { + dlclose(h); + continue; + } + loaded_plugins[num_loaded_plugins].handle = h; + loaded_plugins[num_loaded_plugins].plugin = plug; + num_loaded_plugins++; + } + closedir(dir); +} + +static void +load_plugins(void) +{ + dwm_host_api.dpy = dpy; + dwm_host_api.root = root; + dwm_host_api.screen = screen; + dwm_host_api.sw = sw; + dwm_host_api.sh = sh; + dwm_host_api.bh = bh; + + const char *env_dir = getenv("DWM_PLUGIN_PATH"); + if (env_dir && env_dir[0]) + load_plugins_from_dir(env_dir); + load_plugins_from_dir("/home/user/.local/lib/dwm/plugins"); + load_plugins_from_dir("/usr/lib/dwm/plugins"); +} + +static void +unload_plugins(void) +{ + int i; + for (i = 0; i < num_loaded_plugins; i++) { + if (loaded_plugins[i].plugin && loaded_plugins[i].plugin->cleanup) + loaded_plugins[i].plugin->cleanup(); + if (loaded_plugins[i].handle) + dlclose(loaded_plugins[i].handle); + } + num_loaded_plugins = 0; +} + +static int +dispatch_plugin_softkey_chord(void) +{ + int i; + for (i = 0; i < num_loaded_plugins; i++) { + if (loaded_plugins[i].plugin && loaded_plugins[i].plugin->on_softkey_chord) { + if (loaded_plugins[i].plugin->on_softkey_chord(&dwm_host_api)) + return 1; + } + } + return 0; +} + +static int +dispatch_plugin_idle_key(KeySym keysym, unsigned int keycode, unsigned int state) +{ + int i; + for (i = 0; i < num_loaded_plugins; i++) { + if (loaded_plugins[i].plugin && loaded_plugins[i].plugin->on_idle_key) { + if (loaded_plugins[i].plugin->on_idle_key(&dwm_host_api, keysym, keycode, state)) + return 1; + } + } + return 0; +} + +static int +dispatch_plugin_global_key(KeySym keysym, unsigned int keycode, unsigned int state, int is_press) +{ + int i; + for (i = 0; i < num_loaded_plugins; i++) { + if (loaded_plugins[i].plugin && loaded_plugins[i].plugin->on_global_key) { + if (loaded_plugins[i].plugin->on_global_key(&dwm_host_api, keysym, keycode, state, is_press)) + return 1; + } + } + return 0; +} + static pid_t spawn_pgid(const Arg *arg) { @@ -1547,9 +1735,8 @@ spawn_pgid(const Arg *arg) } static void -trigger_xdrawer_chord(long long now) +trigger_softkey_chord(long long now) { - const Arg xdrawercmd = { .v = (const char*[]){ "xdrawer", NULL } }; if (last_menu_pgid > 0) { kill(-last_menu_pgid, SIGKILL); last_menu_pgid = 0; @@ -1563,7 +1750,10 @@ trigger_xdrawer_chord(long long now) write_softkey_suppress(now + SOFTKEY_SUPPRESS_MS); XUngrabKeyboard(dpy, CurrentTime); XFlush(dpy); - spawn(&xdrawercmd); + if (!dispatch_plugin_softkey_chord()) { + const Arg xdrawercmd = { .v = (const char*[]){ "xdrawer", NULL } }; + spawn(&xdrawercmd); + } } static void @@ -1631,7 +1821,7 @@ keypress(XEvent *e) if (last_softkey_mask != 0 && (now - last_softkey_ms) <= SOFTKEY_CANCEL_WINDOW_MS) combined |= last_softkey_mask; if (combined == (SOFTKEY_LEFT | SOFTKEY_RIGHT)) { - trigger_xdrawer_chord(now); + trigger_softkey_chord(now); } else { softkey_pending = which; last_softkey_mask = which; @@ -1645,6 +1835,16 @@ keypress(XEvent *e) return; } + if (!is_phone_locked() && + dispatch_plugin_global_key(keysym, ev->keycode, CLEANMASK(ev->state), 1)) { + return; + } + + if (!selmon->sel && !is_phone_locked()) { + if (dispatch_plugin_idle_key(keysym, ev->keycode, CLEANMASK(ev->state))) + return; + } + if (!selmon->sel && CLEANMASK(ev->state) == 0) { if (keysym == XK_Return) { const Arg entercmd = { .v = (const char*[]){ "sxmo_appmenu.sh", NULL } }; @@ -2138,7 +2338,7 @@ run(void) int hw_down = query_softkeys_down(); softkey_pending |= hw_down; if (softkey_pending == (SOFTKEY_LEFT | SOFTKEY_RIGHT)) { - trigger_xdrawer_chord(now); + trigger_softkey_chord(now); continue; } long long rem = softkey_deadline_ms - now; @@ -2398,6 +2598,7 @@ setup(void) lidfd = open("/dev/input/by-path/platform-gpio-keys-event", O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (lidfd < 0) lidfd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK | O_CLOEXEC); + load_plugins(); grabkeys(); focus(NULL); } @@ -2648,6 +2849,8 @@ unmapnotify(XEvent *e) setclientstate(c, WithdrawnState); else unmanage(c, 0); + } else if (selmon && !selmon->sel) { + focus(NULL); } } @@ -2658,7 +2861,7 @@ updatebars(void) XSetWindowAttributes wa = { .override_redirect = True, .background_pixmap = ParentRelative, - .event_mask = ButtonPressMask|ExposureMask + .event_mask = ButtonPressMask|ExposureMask|KeyPressMask|KeyReleaseMask }; XClassHint ch = {"dwm", "dwm"}; for (m = mons; m; m = m->next) { diff --git a/src/sxmo-dwm/dwm_plugin.h b/src/sxmo-dwm/dwm_plugin.h new file mode 100644 index 0000000..df37bc0 --- /dev/null +++ b/src/sxmo-dwm/dwm_plugin.h @@ -0,0 +1,45 @@ +#ifndef DWM_PLUGIN_H +#define DWM_PLUGIN_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define DWM_PLUGIN_API_VERSION 1 + +typedef struct DwmHostApi { + int api_version; + Display *dpy; + Window root; + int screen; + int sw; + int sh; + int bh; + int (*is_idle_desktop)(void); + int (*is_phone_locked)(void); + void (*spawn_command)(const char *const *argv); + void (*spawn_plugin)(const char *plugin_name, const char *arg); +} DwmHostApi; + +typedef struct DwmPlugin { + int api_version; + const char *name; + const char *description; + int (*init)(const DwmHostApi *host); + void (*cleanup)(void); + int (*on_softkey_chord)(const DwmHostApi *host); + int (*on_idle_key)(const DwmHostApi *host, KeySym keysym, unsigned int keycode, unsigned int state); + int (*on_global_key)(const DwmHostApi *host, KeySym keysym, unsigned int keycode, unsigned int state, int is_press); + int (*run_main)(int argc, char **argv); +} DwmPlugin; + +#define DWM_PLUGIN_ENTRY_SYMBOL "dwm_plugin_entry" + +#ifdef __cplusplus +} +#endif + +#endif /* DWM_PLUGIN_H */ diff --git a/src/xdrawer/xdrawer.cpp b/src/xdrawer/xdrawer.cpp index 9bb244c..7706c2c 100644 --- a/src/xdrawer/xdrawer.cpp +++ b/src/xdrawer/xdrawer.cpp @@ -14,6 +14,8 @@ #include #include +#include "../sxmo-dwm/dwm_plugin.h" + #include #include #include @@ -433,7 +435,8 @@ class PhoneController { } // namespace -int main() { +static int xdrawer_run_main(int, char**) { + gRunning = 1; int64_t startMs = nowMs(); std::string pidPath = getRuntimeFile("xdrawer.pid"); std::string startPath = getRuntimeFile("xdrawer.start"); @@ -870,3 +873,31 @@ int main() { ::unlink(pidPath.c_str()); return 0; } + +static int xdrawer_plugin_init(const DwmHostApi*) { + return 0; +} + +static int xdrawer_on_softkey_chord(const DwmHostApi* host) { + if (host && host->spawn_plugin) { + host->spawn_plugin("xdrawer", nullptr); + return 1; + } + return 0; +} + +extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { + DWM_PLUGIN_API_VERSION, + "xdrawer", + "Global phone hardware controls drawer (softkey chord)", + xdrawer_plugin_init, + nullptr, + xdrawer_on_softkey_chord, + nullptr, + nullptr, + xdrawer_run_main +}; + +int main(int argc, char** argv) { + return xdrawer_run_main(argc, argv); +}