diff --git a/overlay/rootfs/usr/bin/dwm b/overlay/rootfs/usr/bin/dwm index 48c87cd..2921d1e 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 index f7f12b8..6522237 100755 Binary files a/overlay/rootfs/usr/bin/dwm-plugin 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 d60a1da..e335062 100755 --- a/overlay/rootfs/usr/bin/sxmo_appmenu.sh +++ b/overlay/rootfs/usr/bin/sxmo_appmenu.sh @@ -154,10 +154,10 @@ fast_sys_choices() { $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_phn Dialer^0^xdialer $icon_phl Call Log^0^xcalllog $icon_msg Texts^0^sxmo_modemtext.sh -$icon_usr Contacts^0^sxmo_contactmenu.sh +$icon_usr Contacts^0^xcontacts ${_bt_line}$icon_cam Camera^0^bananarchy-photo --menu ${_flash_line}$icon_net Networks^0^sxmo_networks.sh $icon_mus Audio^0^sxmo_audio.sh diff --git a/overlay/rootfs/usr/bin/sxmo_contactmenu.sh b/overlay/rootfs/usr/bin/sxmo_contactmenu.sh new file mode 100755 index 0000000..dcde3ab --- /dev/null +++ b/overlay/rootfs/usr/bin/sxmo_contactmenu.sh @@ -0,0 +1,5 @@ +#!/bin/sh +if command -v xcontacts >/dev/null 2>&1; then + exec xcontacts "$@" +fi +exec /usr/bin/dwm-plugin contacts "$@" diff --git a/overlay/rootfs/usr/bin/xcontacts b/overlay/rootfs/usr/bin/xcontacts new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/overlay/rootfs/usr/bin/xcontacts @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/overlay/rootfs/usr/lib/dwm/plugins/calllog.so b/overlay/rootfs/usr/lib/dwm/plugins/calllog.so index 6ec43fa..7090c67 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/calllog.so and b/overlay/rootfs/usr/lib/dwm/plugins/calllog.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/contacts.so b/overlay/rootfs/usr/lib/dwm/plugins/contacts.so new file mode 100755 index 0000000..959505d Binary files /dev/null and b/overlay/rootfs/usr/lib/dwm/plugins/contacts.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/dialer.so b/overlay/rootfs/usr/lib/dwm/plugins/dialer.so index 5f27052..5eddbb9 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/dialer.so and b/overlay/rootfs/usr/lib/dwm/plugins/dialer.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so b/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so index 9a8d355..af91d3d 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so and b/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so differ diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_contextmenu.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_contextmenu.sh index 90a70a1..5f429ad 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_contextmenu.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_contextmenu.sh @@ -767,9 +767,10 @@ case "$WMCLASS" in $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_phn Dialer ^ 0 ^ xdialer + $icon_phl Call Log ^ 0 ^ xcalllog $icon_msg Texts ^ 0 ^ sxmo_modemtext.sh - $icon_usr Contacts ^ 0 ^ sxmo_contactmenu.sh + $icon_usr Contacts ^ 0 ^ xcontacts $( bt_is_unblocked && printf %b "$icon_bth Bluetooth ^ 1 ^ sxmo_bluetoothmenu.sh" diff --git a/prebuilt/sxmo-x11/usr/bin/dwm b/prebuilt/sxmo-x11/usr/bin/dwm index 48c87cd..2921d1e 100755 Binary files a/prebuilt/sxmo-x11/usr/bin/dwm and b/prebuilt/sxmo-x11/usr/bin/dwm differ diff --git a/prebuilt/sxmo-x11/usr/bin/dwm-plugin b/prebuilt/sxmo-x11/usr/bin/dwm-plugin index f7f12b8..6522237 100755 Binary files a/prebuilt/sxmo-x11/usr/bin/dwm-plugin and b/prebuilt/sxmo-x11/usr/bin/dwm-plugin differ diff --git a/prebuilt/sxmo-x11/usr/bin/xcontacts b/prebuilt/sxmo-x11/usr/bin/xcontacts new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/prebuilt/sxmo-x11/usr/bin/xcontacts @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so index 6ec43fa..7090c67 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so new file mode 100755 index 0000000..959505d Binary files /dev/null and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so index 5f27052..5eddbb9 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so index 9a8d355..af91d3d 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so differ diff --git a/scripts/build-sxmo-x11.sh b/scripts/build-sxmo-x11.sh index 636cea5..ea9ce07 100755 --- a/scripts/build-sxmo-x11.sh +++ b/scripts/build-sxmo-x11.sh @@ -97,7 +97,7 @@ EOF dwm.c drw.c util.c $DWM_LIBS -o /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm $STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm - echo "[6/6] Compiling DWM Plugins (xdrawer.so, dialer.so, calllog.so) and dwm-plugin runner..." + echo "[6/6] Compiling DWM Plugins (xdrawer.so, dialer.so, calllog.so, contacts.so) and dwm-plugin runner..." mkdir -p /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins PLUG_INCS="$(pkgconf --cflags x11 xft fontconfig freetype2 pango pangoxft alsa)" PLUG_LIBS="$(pkgconf --libs x11 xft fontconfig freetype2 pango pangoxft alsa)" @@ -114,10 +114,15 @@ EOF /bananarchy/src/dwm-plugins/calllog_plugin.cpp $PLUG_LIBS \ -o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so + $CXX $OPT_CFLAGS -fPIC -shared -std=c++17 -Wall -Wextra $PLUG_INCS \ + /bananarchy/src/dwm-plugins/contacts_plugin.cpp $PLUG_LIBS \ + -o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so + $STRIP --strip-unneeded \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so \ - /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so + /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so \ + /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so $CC $OPT_CFLAGS -std=c99 -D_DEFAULT_SOURCE -Wall -Wextra \ /bananarchy/src/dwm-plugins/dwm_plugin_runner.c -ldl \ @@ -128,6 +133,7 @@ EOF ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/XDrawer ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xdialer ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xcalllog + ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xcontacts mkdir -p /bananarchy/overlay/rootfs/usr/bin /bananarchy/overlay/rootfs/usr/lib/dwm/plugins cp -a /bananarchy/prebuilt/sxmo-x11/usr/bin/. /bananarchy/overlay/rootfs/usr/bin/ diff --git a/src/dwm-plugins/calllog_plugin.cpp b/src/dwm-plugins/calllog_plugin.cpp index 934cb66..20768c7 100644 --- a/src/dwm-plugins/calllog_plugin.cpp +++ b/src/dwm-plugins/calllog_plugin.cpp @@ -102,17 +102,17 @@ int calllog_run_main(int argc, char** argv) { PangoLayout* layoutMain = pango_layout_new(pangoCtx); PangoFontDescription* descMain = - pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 14px"); + pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 15px"); 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_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 12px"); 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_font_description_from_string("DejaVuSansM Nerd Font, Fira Code 16px"); pango_layout_set_font_description(layoutIcon, descIcon); auto allocColor = [&](const char* hex) { @@ -324,14 +324,14 @@ int calllog_run_main(int argc, char** argv) { XSetForeground(dpy, gc, 0x444444); XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, 1); - drawPango(layoutSmall, &colLight, 6, sbY + 1, kSoftbarH - 1, "Dialer"); + 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(layoutSmall, rightLabel); - drawPango(layoutSmall, &colLight, kWinWidth - rw - 6, sbY + 1, + int rw = pangoWidth(layoutMain, rightLabel); + drawPango(layoutMain, &colLight, kWinWidth - rw - 6, sbY + 1, kSoftbarH - 1, rightLabel); if (visible) { @@ -350,6 +350,9 @@ int calllog_run_main(int argc, char** argv) { if (isPidFileAlive(getRuntimeFile("xdialer.pid"))) { sendPluginCommand("dialer", "CLOSE"); } + if (isPidFileAlive(getRuntimeFile("xcontacts.pid"))) { + sendPluginCommand("contacts", "CLOSE"); + } if (isPidFileAlive(getRuntimeFile("xdrawer.pid"))) { sendPluginCommand("xdrawer", "CLOSE"); } diff --git a/src/dwm-plugins/contacts_plugin.cpp b/src/dwm-plugins/contacts_plugin.cpp new file mode 100644 index 0000000..7635c20 --- /dev/null +++ b/src/dwm-plugins/contacts_plugin.cpp @@ -0,0 +1,1505 @@ +#include "phone_data.h" +#include "../sxmo-dwm/dwm_plugin.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace bananarchy; + +volatile sig_atomic_t gRunning = 1; + +void handleSignal(int) { + gRunning = 0; +} + +enum class ViewMode { + List = 0, + Detail = 1, + Options = 2, + Edit = 3, + PickRecent = 4, + ConfirmDelete = 5, +}; + +enum class TextCaseMode { + Title = 0, // Abc + Upper = 1, // ABC + Lower = 2, // abc + Num = 3, // 123 +}; + +const char* kMultiTapLower[10] = { + " 0+", // 0 + ".,-@_1", // 1 + "abc2", // 2 + "def3", // 3 + "ghi4", // 4 + "jkl5", // 5 + "mno6", // 6 + "pqrs7", // 7 + "tuv8", // 8 + "wxyz9" // 9 +}; + +int runContactsApp(int argc, char** argv) { + gRunning = 1; + std::setlocale(LC_ALL, ""); + XSetLocaleModifiers(""); + + bool isPreload = false; + std::string initArg; + for (int i = 1; i < argc; ++i) { + if (!argv[i]) continue; + if (std::strcmp(argv[i], "--preload") == 0) { + isPreload = true; + } else if (std::strcmp(argv[i], "newcontact") == 0) { + if (i + 1 < argc && argv[i + 1]) { + initArg = std::string("new:") + argv[++i]; + } else { + initArg = "new:"; + } + } else if (initArg.empty()) { + initArg = argv[i]; + } + } + + std::string daemonPidPath = getPluginDaemonPidPath("contacts"); + std::string activePidPath = getRuntimeFile("xcontacts.pid"); + + int existingDaemon = readIntFile(daemonPidPath, -1); + if (existingDaemon > 0 && existingDaemon != static_cast(::getpid()) && + ::kill(existingDaemon, 0) == 0) { + if (!isPreload) { + sendPluginCommand("contacts", + initArg.empty() ? "OPEN" : ("OPEN " + initArg)); + } + return 0; + } + + writeIntFile(daemonPidPath, static_cast(::getpid())); + int fifoFd = setupPluginDaemonFifo("contacts"); + + struct sigaction sa {}; + sa.sa_handler = handleSignal; + sigemptyset(&sa.sa_mask); + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &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 | + VisibilityChangeMask | StructureNotifyMask; + + Window win = XCreateWindow( + dpy, root, kWinX, kWinY, kWinWidth, kWinHeight, 0, + DefaultDepth(dpy, screen), CopyFromParent, visual, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa); + + XClassHint ch{}; + char resName[] = "xcontacts"; + char resClass[] = "XContacts"; + ch.res_name = resName; + ch.res_class = resClass; + XSetClassHint(dpy, win, &ch); + XStoreName(dpy, win, "Contacts"); + + 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 15px"); + 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 12px"); + 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 16px"); + 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; + std::vector filtered; + std::vector recentUnknowns; + FileStamp contactsStamp; + FileStamp logStamp; + bool contactsLoadedOnce = false; + + ViewMode mode = ViewMode::List; + ViewMode prevModeBeforeEdit = ViewMode::List; + std::string searchQuery; + int selected = 0; + int detailSelected = 0; + int optionSelected = 0; + int confirmSelected = 0; + int recentSelected = 0; + + // Editor state + bool editIsNew = true; + std::string editOrigName; + std::string editOrigNumber; + std::string editName; + std::string editNumber; + int editField = 0; // 0 = Name, 1 = Number, 2 = From Recent, 3 = Save + TextCaseMode caseMode = TextCaseMode::Title; + int lastTapDigit = -1; + int lastTapCycle = 0; + int64_t lastTapMs = 0; + bool zeroHeld = false; + int64_t zeroPressMs = 0; + + bool visible = false; + int64_t openStartMs = 0; + int64_t startupIgnoreUntilMs = 0; + bool initialSoftkeysReleased = true; + int softkeyPending = 0; + int64_t softkeyDeadlineMs = 0; + + auto updateFilter = [&]() { + filtered.clear(); + const auto& all = book.savedContacts(); + if (searchQuery.empty()) { + filtered = all; + } else { + std::string qNorm = normalizePhone(searchQuery); + std::string qLower = toLowerStr(searchQuery); + std::vector pfxMatches; + std::vector subMatches; + for (const auto& c : all) { + bool pfx = (!qLower.empty() && c.lowerName.rfind(qLower, 0) == 0) || + (!qNorm.empty() && c.t9Name.rfind(qNorm, 0) == 0) || + (!qNorm.empty() && c.normNumber.rfind(qNorm, 0) == 0); + bool sub = pfx || + (!qLower.empty() && c.lowerName.find(qLower) != std::string::npos) || + (!qNorm.empty() && c.t9Name.find(qNorm) != std::string::npos) || + (!qNorm.empty() && c.normNumber.find(qNorm) != std::string::npos); + if (pfx) { + pfxMatches.push_back(c); + } else if (sub) { + subMatches.push_back(c); + } + } + filtered.insert(filtered.end(), pfxMatches.begin(), pfxMatches.end()); + filtered.insert(filtered.end(), subMatches.begin(), subMatches.end()); + } + if (selected >= static_cast(filtered.size())) { + selected = std::max(0, static_cast(filtered.size()) - 1); + } + }; + + auto reloadContactsIfNeeded = [&]() -> bool { + bool cChanged = contactsStamp.checkAndUpdate(getContactsFilePath()); + bool lChanged = logStamp.checkAndUpdate(getModemLogFilePath()); + if (cChanged || lChanged || !contactsLoadedOnce) { + contactsLoadedOnce = true; + book.load(); + updateFilter(); + return true; + } + return false; + }; + + reloadContactsIfNeeded(); + + 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 = 38; + constexpr int kSoftbarH = 26; + constexpr int kListY = kHeaderH; + constexpr int kListH = kWinHeight - kHeaderH - kSoftbarH; + constexpr int kVisibleRows = 5; + + auto drawSoftbar = [&](const std::string& leftLabel, + const std::string& centerLabel, + const std::string& rightLabel, + XftColor* centerCol = nullptr) { + 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); + + if (!leftLabel.empty()) { + drawPango(layoutMain, &colLight, 6, sbY + 1, kSoftbarH - 1, leftLabel); + } + if (!centerLabel.empty()) { + int cw = pangoWidth(layoutMain, centerLabel); + drawPango(layoutMain, centerCol ? centerCol : &colGreen, + (kWinWidth - cw) / 2, sbY + 1, kSoftbarH - 1, centerLabel); + } + if (!rightLabel.empty()) { + int rw = pangoWidth(layoutMain, rightLabel); + drawPango(layoutMain, &colLight, kWinWidth - rw - 6, sbY + 1, + kSoftbarH - 1, rightLabel); + } + }; + + auto render = [&]() { + XSetForeground(dpy, gc, 0x222222); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); + + if (mode == ViewMode::List || mode == ViewMode::Options) { + // Header bar with live search / count + XSetForeground(dpy, gc, 0x1c1c1c); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH); + XSetForeground(dpy, gc, 0x444444); + XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1); + + if (searchQuery.empty()) { + drawPango(layoutIcon, &colBlue, 8, 0, kHeaderH, kIconBook); + std::string title = + "Contacts (" + std::to_string(book.savedContacts().size()) + ")"; + drawPango(layoutMain, &colWhite, 32, 0, kHeaderH, title, 200); + } else { + drawPango(layoutIcon, &colYellow, 8, 0, kHeaderH, kIconSearch); + drawPango(layoutMain, &colYellow, 32, 0, kHeaderH, + "Find: " + searchQuery, 178); + drawPango(layoutIcon, &colMuted, 216, 0, kHeaderH, kIconBackspace); + } + + if (filtered.empty()) { + drawPango(layoutIcon, &colMuted, 112, kListY + 46, 24, kIconUser); + drawPango(layoutMain, &colMuted, 32, kListY + 76, 26, + searchQuery.empty() ? "Phonebook is empty" + : "No matching contacts", + 190); + drawPango(layoutSmall, &colMuted, 20, kListY + 106, 22, + "Press Options or Enter to add", 204); + } 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 ContactEntry& c = filtered[idx]; + drawPango(layoutIcon, isSel ? &colYellow : &colBlue, 8, ry, rowH, + kIconUser); + drawPango(layoutMain, isSel ? &colWhite : &colLight, 30, ry + 3, 22, + c.name, 202); + drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 30, ry + 24, 19, + c.number, 202); + } + + 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); + } + } + + if (mode == ViewMode::Options) { + // Popup options menu overlay + const char* optLabels[6] = { + "New Contact", + "Add from Recent", + "Call Contact", + "Send SMS", + "Edit Contact", + "Delete Contact" + }; + const char* optIcons[6] = { + kIconPlus, + kIconClock, + kIconPhone, + kIconSms, + kIconEdit, + kIconTrash + }; + int optCount = filtered.empty() ? 2 : 6; + int menuRowH = 34; + int menuH = optCount * menuRowH + 4; + int menuY = kWinHeight - kSoftbarH - menuH; + XSetForeground(dpy, gc, 0x181818); + XFillRectangle(dpy, pix, gc, 8, menuY, kWinWidth - 16, menuH); + XSetForeground(dpy, gc, 0x005577); + XDrawRectangle(dpy, pix, gc, 8, menuY, kWinWidth - 17, menuH - 1); + + for (int i = 0; i < optCount; ++i) { + int ry = menuY + 2 + i * menuRowH; + bool isSel = (i == optionSelected); + if (isSel) { + XSetForeground(dpy, gc, 0x005577); + XFillRectangle(dpy, pix, gc, 10, ry, kWinWidth - 20, menuRowH); + XSetForeground(dpy, gc, 0xf4d03f); + XFillRectangle(dpy, pix, gc, 10, ry, 4, menuRowH); + } + XftColor* icCol = (i == 5) ? &colRed : (isSel ? &colYellow : &colGreen); + drawPango(layoutIcon, icCol, 18, ry, menuRowH, optIcons[i]); + drawPango(layoutMain, isSel ? &colWhite : &colLight, 42, ry, menuRowH, + optLabels[i], 180); + } + drawSoftbar("Select", "OK", "Back", &colYellow); + } else { + drawSoftbar("Options", filtered.empty() ? "New" : "Open", + searchQuery.empty() ? "Close" : "Clear", &colGreen); + } + } else if (mode == ViewMode::Detail) { + ContactEntry c; + if (selected >= 0 && selected < static_cast(filtered.size())) { + c = filtered[selected]; + } + constexpr int kCardH = 56; + XSetForeground(dpy, gc, 0x1b3b4b); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kCardH); + XSetForeground(dpy, gc, 0xf4d03f); + XFillRectangle(dpy, pix, gc, 0, 0, 4, kCardH); + XSetForeground(dpy, gc, 0x005577); + XFillRectangle(dpy, pix, gc, 0, kCardH - 2, kWinWidth, 2); + + drawPango(layoutIcon, &colYellow, 10, 6, 24, kIconUser); + drawPango(layoutMain, &colWhite, 34, 4, 26, c.name, 198); + drawPango(layoutSmall, &colYellow, 34, 30, 20, c.number, 198); + + const char* actLabels[6] = { + "Call Contact", + "Send SMS", + "Read Messages", + "Open in Dialer", + "Edit Contact", + "Delete Contact" + }; + const char* actIcons[6] = { + kIconPhone, + kIconSms, + kIconList, + kIconPhoneLog, + kIconEdit, + kIconTrash + }; + constexpr int kActCount = 6; + constexpr int kActVis = 5; + int actListY = kCardH; + int actListH = kWinHeight - kCardH - kSoftbarH; + int scrollOffset = 0; + if (detailSelected >= kActVis) { + scrollOffset = detailSelected - kActVis + 1; + } + + for (int i = 0; i < kActVis; ++i) { + int idx = scrollOffset + i; + if (idx >= kActCount) break; + int ry = actListY + (i * actListH) / kActVis; + int nextRy = actListY + ((i + 1) * actListH) / kActVis; + int rowH = nextRy - ry; + bool isSel = (idx == detailSelected); + + 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); + } + XftColor* icCol = + (idx == 5) ? &colRed : (isSel ? &colYellow : &colGreen); + drawPango(layoutIcon, icCol, 10, ry, rowH, actIcons[idx]); + drawPango(layoutMain, + (idx == 5 && !isSel) ? &colRed : (isSel ? &colWhite : &colLight), + 34, ry, rowH, actLabels[idx], 196); + } + + drawSoftbar("Edit", "Select", "Back", &colGreen); + } else if (mode == ViewMode::Edit) { + XSetForeground(dpy, gc, 0x1c1c1c); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH); + XSetForeground(dpy, gc, 0x444444); + XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1); + + drawPango(layoutIcon, &colGreen, 8, 0, kHeaderH, + editIsNew ? kIconPlus : kIconEdit); + drawPango(layoutMain, &colWhite, 30, 0, kHeaderH, + editIsNew ? "New Contact" : "Edit Contact", 145); + + const char* modeNames[4] = {"[Abc]", "[ABC]", "[abc]", "[123]"}; + std::string modeBadge = + (editField == 0) ? modeNames[static_cast(caseMode)] : "[+0-9]"; + int mw = pangoWidth(layoutSmall, modeBadge); + drawPango(layoutSmall, &colYellow, kWinWidth - mw - 6, 0, kHeaderH, + modeBadge); + + // Field 0: Name box (y = 44..108) + auto drawFieldBox = [&](int y, int h, bool active, const char* label, + const std::string& val, const char* placeholder) { + XSetForeground(dpy, gc, active ? 0x1b3b4b : 0x2a2a2a); + XFillRectangle(dpy, pix, gc, 6, y, kWinWidth - 12, h); + XSetForeground(dpy, gc, active ? 0xf4d03f : 0x444444); + XDrawRectangle(dpy, pix, gc, 6, y, kWinWidth - 13, h - 1); + drawPango(layoutSmall, active ? &colYellow : &colMuted, 12, y + 3, 18, + label); + std::string shown = val; + if (active) { + shown.push_back('|'); + } + if (val.empty() && !active) { + drawPango(layoutMain, &colMuted, 12, y + 23, 28, placeholder, 214); + } else { + if (shown.size() > 21) { + shown = ".." + shown.substr(shown.size() - 19); + } + drawPango(layoutMain, &colWhite, 12, y + 23, 28, shown, 214); + } + }; + + drawFieldBox(44, 56, editField == 0, "Name (# changes case):", editName, + "Type name on keypad..."); + drawFieldBox(106, 56, editField == 1, + "Number (Hold 0 for +):", editNumber, "Enter phone number..."); + + // Button Row 2: Pick from Recent Numbers (y = 168..208) + auto drawBtnRow = [&](int y, int h, bool active, const char* icon, + const char* label, XftColor* icCol) { + XSetForeground(dpy, gc, active ? 0x005577 : 0x2c2c2c); + XFillRectangle(dpy, pix, gc, 6, y, kWinWidth - 12, h); + if (active) { + XSetForeground(dpy, gc, 0xf4d03f); + XFillRectangle(dpy, pix, gc, 6, y, 4, h); + } + drawPango(layoutIcon, active ? &colYellow : icCol, 14, y, h, icon); + drawPango(layoutMain, active ? &colWhite : &colLight, 38, y, h, label, + 190); + }; + + drawBtnRow(168, 42, editField == 2, kIconClock, "Use Recent Number", + &colBlue); + drawBtnRow(216, 44, editField == 3, kIconCheck, "Save Contact", + &colGreen); + + bool canClearChar = + (editField == 0 && !editName.empty()) || + (editField == 1 && !editNumber.empty()); + drawSoftbar("Save", (editField >= 2) ? "Select" : "Next", + canClearChar ? "Clear" : "Cancel", &colGreen); + } else if (mode == ViewMode::PickRecent) { + XSetForeground(dpy, gc, 0x1c1c1c); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH); + XSetForeground(dpy, gc, 0x444444); + XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1); + drawPango(layoutIcon, &colBlue, 8, 0, kHeaderH, kIconClock); + drawPango(layoutMain, &colWhite, 32, 0, kHeaderH, "Recent Numbers", 200); + + if (recentUnknowns.empty()) { + drawPango(layoutMain, &colMuted, 24, kListY + 70, 28, + "No recent numbers", 196); + } else { + int total = static_cast(recentUnknowns.size()); + int scrollOffset = 0; + if (recentSelected >= kVisibleRows) { + scrollOffset = recentSelected - kVisibleRows + 1; + } + 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 == recentSelected); + 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); + } + drawPango(layoutIcon, isSel ? &colYellow : &colGreen, 10, ry, rowH, + kIconPhone); + drawPango(layoutMain, isSel ? &colWhite : &colLight, 34, ry, rowH, + recentUnknowns[idx], 198); + } + } + drawSoftbar("Use", "Select", "Back", &colGreen); + } else if (mode == ViewMode::ConfirmDelete) { + XSetForeground(dpy, gc, 0x1c1c1c); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH); + drawPango(layoutIcon, &colRed, 8, 0, kHeaderH, kIconTrash); + drawPango(layoutMain, &colWhite, 32, 0, kHeaderH, "Delete Contact?", 200); + + ContactEntry c; + if (selected >= 0 && selected < static_cast(filtered.size())) { + c = filtered[selected]; + } + drawPango(layoutMain, &colWhite, 14, 54, 28, c.name, 212); + drawPango(layoutSmall, &colYellow, 14, 82, 22, c.number, 212); + + const char* ynLabels[2] = {"No, keep contact", "Yes, delete contact"}; + for (int i = 0; i < 2; ++i) { + int ry = 130 + i * 50; + bool isSel = (i == confirmSelected); + XSetForeground(dpy, gc, isSel ? 0x005577 : 0x2c2c2c); + XFillRectangle(dpy, pix, gc, 8, ry, kWinWidth - 16, 44); + if (isSel) { + XSetForeground(dpy, gc, 0xf4d03f); + XFillRectangle(dpy, pix, gc, 8, ry, 4, 44); + } + drawPango(layoutIcon, (i == 1) ? &colRed : &colGreen, 16, ry, 44, + (i == 1) ? kIconTrash : kIconCheck); + drawPango(layoutMain, isSel ? &colWhite : &colLight, 40, ry, 44, + ynLabels[i], 184); + } + + drawSoftbar("Delete", "Select", "Cancel", &colRed); + } + + 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(kIconBook) + kIconUser + kIconSearch + kIconPlus + + kIconEdit + kIconTrash + kIconSms + kIconList + kIconCheck + + kIconPhone + kIconClock); + render(); + XSync(dpy, False); + + auto beginNewContact = [&](const std::string& prefillNumber) { + prevModeBeforeEdit = mode; + editIsNew = true; + editOrigName.clear(); + editOrigNumber.clear(); + editName.clear(); + editNumber = normalizePhone(prefillNumber); + editField = 0; + caseMode = TextCaseMode::Title; + lastTapDigit = -1; + mode = ViewMode::Edit; + }; + + auto beginEditContact = [&](const ContactEntry& c) { + prevModeBeforeEdit = mode; + editIsNew = false; + editOrigName = c.name; + editOrigNumber = c.number; + editName = c.name; + editNumber = c.number; + editField = 0; + caseMode = TextCaseMode::Title; + lastTapDigit = -1; + mode = ViewMode::Edit; + }; + + auto saveEditedContact = [&]() -> bool { + std::string nm = trimStr(editName); + std::string num = normalizePhone(editNumber); + if (nm.empty() || num.empty()) { + return false; + } + if (!book.addOrUpdateContact(editIsNew ? "" : editOrigNumber, + editIsNew ? "" : editOrigName, num, nm)) { + return false; + } + contactsStamp.checkAndUpdate(getContactsFilePath()); + searchQuery.clear(); + updateFilter(); + std::string targetLower = toLowerStr(nm); + for (size_t i = 0; i < filtered.size(); ++i) { + if (filtered[i].lowerName == targetLower && + filtered[i].normNumber == num) { + selected = static_cast(i); + break; + } + } + mode = ViewMode::List; + return true; + }; + + auto showWindow = [&](const std::string& arg) { + if (isPidFileAlive(getRuntimeFile("xdialer.pid"))) { + sendPluginCommand("dialer", "CLOSE"); + } + if (isPidFileAlive(getRuntimeFile("xcalllog.pid"))) { + sendPluginCommand("calllog", "CLOSE"); + } + if (isPidFileAlive(getRuntimeFile("xdrawer.pid"))) { + sendPluginCommand("xdrawer", "CLOSE"); + } + bool openNewMode = (arg.rfind("new:", 0) == 0); + std::string prefill = openNewMode ? arg.substr(4) : ""; + + if (!visible) { + openStartMs = nowMs(); + startupIgnoreUntilMs = openStartMs + 90; + initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); + softkeyPending = 0; + softkeyDeadlineMs = 0; + zeroHeld = false; + visible = true; + XMapRaised(dpy, win); + if (!openNewMode) { + 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 (openNewMode) { + beginNewContact(prefill); + render(); + } else if (reloaded || mode != ViewMode::List || !searchQuery.empty() || + selected != 0) { + mode = ViewMode::List; + searchQuery.clear(); + selected = 0; + updateFilter(); + render(); + } + XFlush(dpy); + writeIntFile(activePidPath, static_cast(::getpid())); + } else { + reloadContactsIfNeeded(); + if (openNewMode) { + beginNewContact(prefill); + } + 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 (mode != ViewMode::List || !searchQuery.empty() || selected != 0) { + mode = ViewMode::List; + searchQuery.clear(); + selected = 0; + updateFilter(); + render(); + XFlush(dpy); + } + }; + + if (!isPreload) { + showWindow(initArg); + } + + int xfd = ConnectionNumber(dpy); + + auto handleMultiTapNameChar = [&](int digit) { + if (digit < 0 || digit > 9) return; + int64_t now = nowMs(); + if (caseMode == TextCaseMode::Num) { + editName.push_back(static_cast('0' + digit)); + lastTapDigit = -1; + return; + } + const char* seq = kMultiTapLower[digit]; + int seqLen = static_cast(std::strlen(seq)); + if (digit == lastTapDigit && (now - lastTapMs) <= 650 && !editName.empty()) { + lastTapCycle = (lastTapCycle + 1) % seqLen; + char ch = seq[lastTapCycle]; + bool makeUpper = false; + if (caseMode == TextCaseMode::Upper) { + makeUpper = true; + } else if (caseMode == TextCaseMode::Title) { + makeUpper = (editName.size() == 1 || + editName[editName.size() - 2] == ' '); + } + if (makeUpper) { + ch = static_cast(std::toupper(static_cast(ch))); + } + editName.back() = ch; + } else { + lastTapDigit = digit; + lastTapCycle = 0; + char ch = seq[0]; + bool makeUpper = false; + if (caseMode == TextCaseMode::Upper) { + makeUpper = true; + } else if (caseMode == TextCaseMode::Title) { + makeUpper = (editName.empty() || editName.back() == ' '); + } + if (makeUpper) { + ch = static_cast(std::toupper(static_cast(ch))); + } + editName.push_back(ch); + } + lastTapMs = now; + }; + + auto executeDetailAction = [&](int actIdx) { + if (selected < 0 || selected >= static_cast(filtered.size())) { + return; + } + ContactEntry c = filtered[selected]; + switch (actIdx) { + case 0: // Call Contact + writeSoftkeySuppress(nowMs() + 350); + hideWindow(true); + placePhoneCall(c.number); + break; + case 1: // Send SMS + writeSoftkeySuppress(nowMs() + 350); + hideWindow(true); + runAsyncCmd("sxmo_modemtext.sh sendtextmenu '" + c.number + "'"); + break; + case 2: // Read Messages + writeSoftkeySuppress(nowMs() + 350); + hideWindow(true); + runAsyncCmd("sxmo_hook_tailtextlog.sh '" + c.number + "'"); + break; + case 3: // Open in Dialer + writeSoftkeySuppress(nowMs() + 300); + hideWindow(false); + if (!sendPluginCommand("dialer", "OPEN " + c.number)) { + runAsyncArgv("xdialer", c.number.c_str()); + } + break; + case 4: // Edit Contact + beginEditContact(c); + render(); + break; + case 5: // Delete Contact + confirmSelected = 0; + mode = ViewMode::ConfirmDelete; + render(); + break; + } + }; + + auto executeOptionAction = [&](int optIdx) { + if (filtered.empty()) { + if (optIdx == 0) { + beginNewContact(searchQuery); + render(); + } else if (optIdx == 1) { + recentUnknowns = book.unknownRecentNumbers(); + recentSelected = 0; + beginNewContact(""); + mode = ViewMode::PickRecent; + render(); + } + return; + } + switch (optIdx) { + case 0: // New Contact + beginNewContact(""); + render(); + break; + case 1: // Add from Recent + recentUnknowns = book.unknownRecentNumbers(); + recentSelected = 0; + beginNewContact(""); + mode = ViewMode::PickRecent; + render(); + break; + case 2: // Call + executeDetailAction(0); + break; + case 3: // Send SMS + executeDetailAction(1); + break; + case 4: // Edit + executeDetailAction(4); + break; + case 5: // Delete + executeDetailAction(5); + break; + } + }; + + while (gRunning) { + for (const std::string& cmd : readFifoLines(fifoFd)) { + if (cmd == "NOP") { + continue; + } else if (cmd == "CLOSE") { + hideWindow(false); + } else if (cmd == "TOGGLE") { + if (visible) { + hideWindow(false); + } else { + showWindow(""); + } + } else if (cmd == "OPEN") { + showWindow(""); + } else if (cmd.rfind("OPEN ", 0) == 0) { + showWindow(cmd.substr(5)); + } + } + + if (visible && mode == ViewMode::Edit && editField == 1 && zeroHeld) { + if (!isKeycodeDown(dpy, 19)) { + zeroHeld = false; + } else if (nowMs() - zeroPressMs >= 420) { + zeroHeld = false; + if (!editNumber.empty() && editNumber.back() == '0') { + editNumber.back() = '+'; + 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) { + 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); + hideWindow(false); + if (!sendPluginCommand("xdrawer", "TOGGLE")) { + runAsyncArgv("xdrawer"); + } + break; + } + if (softkeyDeadlineMs == 0) { + softkeyDeadlineMs = nowMs() + 40; + } + continue; + } + + if (sym == XK_Escape) { + if (mode == ViewMode::List) { + writeSoftkeySuppress(nowMs() + 300); + hideWindow(true); + } else if (mode == ViewMode::PickRecent) { + mode = ViewMode::Edit; + render(); + } else { + mode = ViewMode::List; + render(); + } + continue; + } + + // Handle mode-specific keys + if (mode == ViewMode::List) { + char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state); + if (dialCh >= '0' && dialCh <= '9') { + searchQuery.push_back(dialCh); + selected = 0; + updateFilter(); + render(); + continue; + } + switch (sym) { + case XK_BackSpace: + case XK_Left: + case XK_KP_Left: + if (!searchQuery.empty()) { + searchQuery.pop_back(); + selected = 0; + updateFilter(); + render(); + } else if (sym == XK_BackSpace) { + hideWindow(true); + } + break; + case XK_Up: + case XK_KP_Up: + if (!filtered.empty()) { + selected = (selected <= 0) + ? (static_cast(filtered.size()) - 1) + : (selected - 1); + render(); + } + break; + case XK_Down: + case XK_KP_Down: + if (!filtered.empty()) { + selected = (selected + 1 >= static_cast(filtered.size())) + ? 0 + : (selected + 1); + render(); + } + break; + case XF86XK_Phone: + case XF86XK_PickupPhone: + if (selected >= 0 && selected < static_cast(filtered.size())) { + std::string target = filtered[selected].number; + writeSoftkeySuppress(nowMs() + 350); + hideWindow(true); + placePhoneCall(target); + } + break; + case XK_Return: + case XK_KP_Enter: + if (filtered.empty()) { + beginNewContact(searchQuery); + } else { + detailSelected = 0; + mode = ViewMode::Detail; + } + render(); + break; + default: + if (ev.xkey.keycode == 177 && selected >= 0 && + selected < static_cast(filtered.size())) { + std::string target = filtered[selected].number; + writeSoftkeySuppress(nowMs() + 350); + hideWindow(true); + placePhoneCall(target); + } + break; + } + } else if (mode == ViewMode::Options) { + int optCount = filtered.empty() ? 2 : 6; + switch (sym) { + case XK_Up: + case XK_KP_Up: + optionSelected = + (optionSelected <= 0) ? (optCount - 1) : (optionSelected - 1); + render(); + break; + case XK_Down: + case XK_KP_Down: + optionSelected = + (optionSelected + 1 >= optCount) ? 0 : (optionSelected + 1); + render(); + break; + case XK_Return: + case XK_KP_Enter: + executeOptionAction(optionSelected); + break; + case XK_BackSpace: + mode = ViewMode::List; + render(); + break; + default: + break; + } + } else if (mode == ViewMode::Detail) { + constexpr int kActCount = 6; + switch (sym) { + case XK_Up: + case XK_KP_Up: + detailSelected = + (detailSelected <= 0) ? (kActCount - 1) : (detailSelected - 1); + render(); + break; + case XK_Down: + case XK_KP_Down: + detailSelected = + (detailSelected + 1 >= kActCount) ? 0 : (detailSelected + 1); + render(); + break; + case XK_Return: + case XK_KP_Enter: + executeDetailAction(detailSelected); + break; + case XF86XK_Phone: + case XF86XK_PickupPhone: + executeDetailAction(0); + break; + case XK_BackSpace: + case XK_Left: + case XK_KP_Left: + mode = ViewMode::List; + render(); + break; + default: + if (ev.xkey.keycode == 177) { + executeDetailAction(0); + } + break; + } + } else if (mode == ViewMode::Edit) { + char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state); + if (editField == 0) { + if (dialCh == '#') { + caseMode = static_cast( + (static_cast(caseMode) + 1) % 4); + lastTapDigit = -1; + render(); + continue; + } + if (dialCh == '*') { + editName.push_back(' '); + lastTapDigit = -1; + render(); + continue; + } + if (dialCh >= '0' && dialCh <= '9') { + handleMultiTapNameChar(dialCh - '0'); + render(); + continue; + } + if (sym >= XK_a && sym <= XK_z) { + char c = static_cast('a' + (sym - XK_a)); + if (ev.xkey.state & ShiftMask) { + c = static_cast(std::toupper(static_cast(c))); + } + editName.push_back(c); + lastTapDigit = -1; + render(); + continue; + } + if (sym == XK_space) { + editName.push_back(' '); + lastTapDigit = -1; + render(); + continue; + } + } else if (editField == 1) { + if (dialCh != '\0') { + editNumber.push_back(dialCh); + if (dialCh == '0') { + zeroHeld = true; + zeroPressMs = nowMs(); + } else { + zeroHeld = false; + } + render(); + continue; + } + } + + switch (sym) { + case XK_Up: + case XK_KP_Up: + editField = (editField <= 0) ? 3 : (editField - 1); + lastTapDigit = -1; + render(); + break; + case XK_Down: + case XK_KP_Down: + editField = (editField >= 3) ? 0 : (editField + 1); + lastTapDigit = -1; + render(); + break; + case XK_BackSpace: + case XK_Left: + case XK_KP_Left: + lastTapDigit = -1; + if (editField == 0 && !editName.empty()) { + editName.pop_back(); + render(); + } else if (editField == 1 && !editNumber.empty()) { + editNumber.pop_back(); + render(); + } else if (sym == XK_BackSpace) { + mode = prevModeBeforeEdit; + render(); + } + break; + case XK_Right: + case XK_KP_Right: + lastTapDigit = -1; + if (editField == 1 && + (editNumber.empty() || editNumber[0] != '+')) { + editNumber = "+" + editNumber; + render(); + } + break; + case XK_Return: + case XK_KP_Enter: + lastTapDigit = -1; + if (editField == 0) { + editField = 1; + render(); + } else if (editField == 1) { + if (!saveEditedContact()) { + editField = editName.empty() ? 0 : 1; + } + render(); + } else if (editField == 2) { + recentUnknowns = book.unknownRecentNumbers(); + recentSelected = 0; + mode = ViewMode::PickRecent; + render(); + } else if (editField == 3) { + if (!saveEditedContact()) { + editField = editName.empty() ? 0 : 1; + } + render(); + } + break; + default: + break; + } + } else if (mode == ViewMode::PickRecent) { + switch (sym) { + case XK_Up: + case XK_KP_Up: + if (!recentUnknowns.empty()) { + recentSelected = + (recentSelected <= 0) + ? (static_cast(recentUnknowns.size()) - 1) + : (recentSelected - 1); + render(); + } + break; + case XK_Down: + case XK_KP_Down: + if (!recentUnknowns.empty()) { + recentSelected = + (recentSelected + 1 >= static_cast(recentUnknowns.size())) + ? 0 + : (recentSelected + 1); + render(); + } + break; + case XK_Return: + case XK_KP_Enter: + if (recentSelected >= 0 && + recentSelected < static_cast(recentUnknowns.size())) { + editNumber = recentUnknowns[recentSelected]; + editField = editName.empty() ? 0 : 3; + } + mode = ViewMode::Edit; + render(); + break; + case XK_BackSpace: + case XK_Left: + case XK_KP_Left: + mode = ViewMode::Edit; + render(); + break; + default: + break; + } + } else if (mode == ViewMode::ConfirmDelete) { + switch (sym) { + case XK_Up: + case XK_KP_Up: + case XK_Down: + case XK_KP_Down: + confirmSelected = 1 - confirmSelected; + render(); + break; + case XK_Return: + case XK_KP_Enter: + if (confirmSelected == 1 && selected >= 0 && + selected < static_cast(filtered.size())) { + book.deleteContact(filtered[selected].number, + filtered[selected].name); + contactsStamp.checkAndUpdate(getContactsFilePath()); + updateFilter(); + mode = ViewMode::List; + } else { + mode = ViewMode::Detail; + } + render(); + break; + case XK_BackSpace: + mode = ViewMode::Detail; + render(); + 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) { + // Left softkey + if (mode == ViewMode::List) { + optionSelected = 0; + mode = ViewMode::Options; + render(); + } else if (mode == ViewMode::Options) { + executeOptionAction(optionSelected); + } else if (mode == ViewMode::Detail) { + if (selected >= 0 && selected < static_cast(filtered.size())) { + beginEditContact(filtered[selected]); + render(); + } + } else if (mode == ViewMode::Edit) { + if (!saveEditedContact()) { + editField = editName.empty() ? 0 : 1; + } + render(); + } else if (mode == ViewMode::PickRecent) { + if (recentSelected >= 0 && + recentSelected < static_cast(recentUnknowns.size())) { + editNumber = recentUnknowns[recentSelected]; + editField = editName.empty() ? 0 : 3; + } + mode = ViewMode::Edit; + render(); + } else if (mode == ViewMode::ConfirmDelete) { + if (selected >= 0 && selected < static_cast(filtered.size())) { + book.deleteContact(filtered[selected].number, + filtered[selected].name); + contactsStamp.checkAndUpdate(getContactsFilePath()); + updateFilter(); + } + mode = ViewMode::List; + render(); + } + } else if (comb == 2) { + // Right softkey + if (mode == ViewMode::List) { + if (!searchQuery.empty()) { + searchQuery.clear(); + selected = 0; + updateFilter(); + render(); + } else { + writeSoftkeySuppress(nowMs() + 350); + hideWindow(true); + } + } else if (mode == ViewMode::Options) { + mode = ViewMode::List; + render(); + } else if (mode == ViewMode::Detail) { + mode = ViewMode::List; + render(); + } else if (mode == ViewMode::Edit) { + lastTapDigit = -1; + if (editField == 0 && !editName.empty()) { + editName.pop_back(); + render(); + } else if (editField == 1 && !editNumber.empty()) { + editNumber.pop_back(); + render(); + } else { + mode = (prevModeBeforeEdit == ViewMode::Detail) ? ViewMode::Detail + : ViewMode::List; + render(); + } + } else if (mode == ViewMode::PickRecent) { + mode = ViewMode::Edit; + render(); + } else if (mode == ViewMode::ConfirmDelete) { + mode = ViewMode::Detail; + render(); + } + } + } + + int timeoutMs = -1; + if (visible) { + timeoutMs = 200; + if (softkeyDeadlineMs > 0) { + timeoutMs = + static_cast(std::max(5, softkeyDeadlineMs - nowMs())); + } + if (zeroHeld) { + int64_t rem = 420 - (nowMs() - zeroPressMs); + timeoutMs = std::min(timeoutMs, static_cast(std::max(5, rem))); + } + } + + 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("contacts").c_str()); + ::unlink(daemonPidPath.c_str()); + + 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); + return 0; +} + +int contacts_plugin_init(const DwmHostApi* host) { + if (host && host->spawn_plugin) { + host->spawn_plugin("contacts", "--preload"); + } + return 0; +} + +int contacts_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_Back || keysym == XF86XK_KbdLcdMenu2 || keycode == 166) { + if (host->spawn_plugin) { + host->spawn_plugin("contacts", nullptr); + } + return 1; + } + return 0; +} + +} // namespace + +extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { + DWM_PLUGIN_API_VERSION, + "contacts", + "Keypad-driven phonebook & contact manager (opens on idle Right Softkey / Names)", + contacts_plugin_init, + nullptr, + nullptr, + contacts_on_idle_key, + nullptr, + runContactsApp +}; diff --git a/src/dwm-plugins/dialer_plugin.cpp b/src/dwm-plugins/dialer_plugin.cpp index 0e590a9..26f23a1 100644 --- a/src/dwm-plugins/dialer_plugin.cpp +++ b/src/dwm-plugins/dialer_plugin.cpp @@ -112,17 +112,17 @@ int dialer_run_main(int argc, char** argv) { PangoLayout* layoutMain = pango_layout_new(pangoCtx); PangoFontDescription* descMain = - pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 14px"); + pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 15px"); 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_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 12px"); 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_font_description_from_string("DejaVuSansM Nerd Font, Fira Code 16px"); pango_layout_set_font_description(layoutIcon, descIcon); auto allocColor = [&](const char* hex) { @@ -360,12 +360,12 @@ int dialer_run_main(int argc, char** argv) { std::string centerLabel = "CALL"; std::string rightLabel = number.empty() ? "Close" : "Clear"; - drawPango(layoutSmall, &colLight, 6, sbY + 1, kSoftbarH - 1, leftLabel); + 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(layoutSmall, rightLabel); - drawPango(layoutSmall, &colLight, kWinWidth - rw - 6, sbY + 1, + int rw = pangoWidth(layoutMain, rightLabel); + drawPango(layoutMain, &colLight, kWinWidth - rw - 6, sbY + 1, kSoftbarH - 1, rightLabel); if (visible) { @@ -389,6 +389,9 @@ int dialer_run_main(int argc, char** argv) { if (isPidFileAlive(getRuntimeFile("xcalllog.pid"))) { sendPluginCommand("calllog", "CLOSE"); } + if (isPidFileAlive(getRuntimeFile("xcontacts.pid"))) { + sendPluginCommand("contacts", "CLOSE"); + } if (isPidFileAlive(getRuntimeFile("xdrawer.pid"))) { sendPluginCommand("xdrawer", "CLOSE"); } diff --git a/src/dwm-plugins/dwm_plugin_runner.c b/src/dwm-plugins/dwm_plugin_runner.c index 898129e..27dd873 100644 --- a/src/dwm-plugins/dwm_plugin_runner.c +++ b/src/dwm-plugins/dwm_plugin_runner.c @@ -38,6 +38,9 @@ static int try_send_plugin_fifo(const char *plugin_name, int sub_argc, char **su int len = 0; if (strcmp(plugin_name, "xdrawer") == 0) { len = snprintf(cmd, sizeof(cmd), "TOGGLE\n"); + } else if (sub_argc >= 2 && sub_argv[1] && strcmp(sub_argv[1], "newcontact") == 0) { + const char *num = (sub_argc >= 3 && sub_argv[2]) ? sub_argv[2] : ""; + len = snprintf(cmd, sizeof(cmd), "OPEN new:%s\n", num); } else if (sub_argc >= 2 && sub_argv[1] && sub_argv[1][0]) { len = snprintf(cmd, sizeof(cmd), "OPEN %s\n", sub_argv[1]); } else { @@ -67,6 +70,9 @@ static const char *resolve_plugin_name_from_argv0(const char *argv0) { if (strcasecmp(base, "xcalllog") == 0 || strcasecmp(base, "calllog") == 0) { return "calllog"; } + if (strcasecmp(base, "xcontacts") == 0 || strcasecmp(base, "contacts") == 0) { + return "contacts"; + } return NULL; } diff --git a/src/dwm-plugins/phone_data.h b/src/dwm-plugins/phone_data.h index 8f28515..7ba8aed 100644 --- a/src/dwm-plugins/phone_data.h +++ b/src/dwm-plugins/phone_data.h @@ -40,11 +40,19 @@ constexpr int kWinHeight = kScreenHeight - kTopBarHeight; // 296px flush below 2 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* kIconBook = "\xef\x83\x80"; // U+F0C0 address book / users 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 +constexpr const char* kIconSearch = "\xef\x80\x82"; // U+F002 search +constexpr const char* kIconPlus = "\xef\x81\xa7"; // U+F067 plus/add +constexpr const char* kIconEdit = "\xef\x81\x84"; // U+F044 edit +constexpr const char* kIconTrash = "\xef\x87\xb8"; // U+F1F8 delete/trash +constexpr const char* kIconSms = "\xef\x83\xa0"; // U+F0E0 envelope/SMS +constexpr const char* kIconList = "\xef\x80\xba"; // U+F03A list +constexpr const char* kIconCheck = "\xef\x80\x8c"; // U+F00C check inline int64_t nowMs() { struct timespec ts {}; @@ -525,6 +533,176 @@ class ContactBook { return out; } + const std::vector& savedContacts() const { return saved_; } + + std::vector unknownRecentNumbers() const { + std::vector out; + std::unordered_set seen; + 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() || !resolveName(num).empty()) { + continue; + } + std::string key = phoneSuffixKey(norm); + if (seen.count(key) || seen.count(norm)) { + continue; + } + seen.insert(key); + seen.insert(norm); + out.push_back(num); + } + return out; + } + + static void ensureParentDir(const std::string& filePath) { + size_t slash = filePath.rfind('/'); + if (slash == std::string::npos || slash == 0) { + return; + } + std::string dir = filePath.substr(0, slash); + std::string cur; + for (size_t i = 0; i < dir.size(); ++i) { + cur.push_back(dir[i]); + if (dir[i] == '/' && cur.size() > 1) { + ::mkdir(cur.c_str(), 0755); + } + } + ::mkdir(dir.c_str(), 0755); + } + + bool addOrUpdateContact(const std::string& oldNumber, + const std::string& oldName, + const std::string& newNumberRaw, + const std::string& newNameRaw) { + std::string newNumber = trimStr(newNumberRaw); + std::string newName = trimStr(newNameRaw); + for (char& ch : newName) { + if (ch == '\t' || ch == '\r' || ch == '\n') { + ch = ' '; + } + } + if (newNumber.empty() || newName.empty()) { + return false; + } + std::string path = getContactsFilePath(); + ensureParentDir(path); + + std::vector> rows; + { + std::ifstream in(path); + std::string line; + while (std::getline(in, 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)); + if (!num.empty() && !name.empty()) { + rows.push_back({num, name}); + } + } + } + + bool replaced = false; + std::string oldNorm = normalizePhone(oldNumber); + std::string newNorm = normalizePhone(newNumber); + for (auto& r : rows) { + if (!oldNumber.empty() && + ((r.first == oldNumber && r.second == oldName) || + (!oldNorm.empty() && normalizePhone(r.first) == oldNorm && + r.second == oldName))) { + r.first = newNumber; + r.second = newName; + replaced = true; + break; + } + } + if (!replaced && oldNumber.empty()) { + for (auto& r : rows) { + if (r.first == newNumber || + (!newNorm.empty() && normalizePhone(r.first) == newNorm)) { + r.first = newNumber; + r.second = newName; + replaced = true; + break; + } + } + } + if (!replaced) { + rows.push_back({newNumber, newName}); + } + + std::ofstream out(path, std::ios::trunc); + if (!out.is_open()) { + return false; + } + for (const auto& r : rows) { + out << r.first << "\t" << r.second << "\n"; + } + out.close(); + load(); + return true; + } + + bool deleteContact(const std::string& number, const std::string& name) { + std::string path = getContactsFilePath(); + std::vector> rows; + std::string targetNorm = normalizePhone(number); + bool removed = false; + { + std::ifstream in(path); + std::string line; + while (std::getline(in, 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 nm = trimStr(line.substr(tab + 1)); + if (num.empty() || nm.empty()) continue; + if (!removed && + ((num == number && nm == name) || + (!targetNorm.empty() && normalizePhone(num) == targetNorm && + nm == name))) { + removed = true; + continue; + } + rows.push_back({num, nm}); + } + } + if (!removed) { + return false; + } + std::ofstream out(path, std::ios::trunc); + if (!out.is_open()) { + return false; + } + for (const auto& r : rows) { + out << r.first << "\t" << r.second << "\n"; + } + out.close(); + load(); + return true; + } + private: std::vector saved_; std::unordered_map exactMap_; diff --git a/src/sxmo-dwm/dwm.c b/src/sxmo-dwm/dwm.c index 4b2c407..3943b38 100644 --- a/src/sxmo-dwm/dwm.c +++ b/src/sxmo-dwm/dwm.c @@ -49,6 +49,7 @@ #include #include #include +#include #include "drw.h" #include "util.h" @@ -147,6 +148,7 @@ struct Monitor { Client *stack; Monitor *next; Window barwin; + Window softwin; const Layout *lt[2]; }; @@ -769,6 +771,10 @@ cleanupmon(Monitor *mon) } XUnmapWindow(dpy, mon->barwin); XDestroyWindow(dpy, mon->barwin); + if (mon->softwin) { + XUnmapWindow(dpy, mon->softwin); + XDestroyWindow(dpy, mon->softwin); + } free(mon); } @@ -1021,6 +1027,35 @@ drawbar(Monitor *m) drw_text(drw, MAX(x, m->ww - sw), 0, sw, bh, 0, s, 0, True); } drw_map(drw, m->barwin, 0, 0, m->ww, bh); + if (m->softwin && drw && drw->font) { + XLowerWindow(dpy, m->softwin); + XClearWindow(dpy, m->softwin); + if (!m->sel) { + XftDraw *d = XftDrawCreate(dpy, m->softwin, + DefaultVisual(dpy, screen), DefaultColormap(dpy, screen)); + if (d) { + unsigned int ew = 0, eh = 0; + const char *left_txt = "Menu"; + const char *right_txt = "Names"; + int sh_bar = 26; + + drw_font_getexts(drw->font, left_txt, strlen(left_txt), &ew, &eh, False); + int ty = (sh_bar - (int)eh) / 2; + pango_layout_set_text(drw->font->layout, left_txt, strlen(left_txt)); + pango_xft_render_layout(d, &scheme[SchemeSel][ColFg], + drw->font->layout, 6 * PANGO_SCALE, ty * PANGO_SCALE); + + drw_font_getexts(drw->font, right_txt, strlen(right_txt), &ew, &eh, False); + ty = (sh_bar - (int)eh) / 2; + int rx = m->ww - (int)ew - 6; + pango_layout_set_text(drw->font->layout, right_txt, strlen(right_txt)); + pango_xft_render_layout(d, &scheme[SchemeSel][ColFg], + drw->font->layout, rx * PANGO_SCALE, ty * PANGO_SCALE); + + XftDrawDestroy(d); + } + } + } } void @@ -1806,6 +1841,10 @@ flush_pending_softkey(void) const Arg syscmd = { .v = (const char*[]){ "sxmo_appmenu.sh", "sys", NULL } }; last_menu_pgid = spawn_pgid(&syscmd); } else if (pending == SOFTKEY_RIGHT) { + if (host_is_idle_desktop() && + dispatch_plugin_idle_key(XF86XK_Back, 166, 0)) { + return; + } const Arg ctxcmd = { .v = (const char*[]){ "sxmo_appmenu.sh", NULL } }; last_menu_pgid = spawn_pgid(&ctxcmd); } @@ -2170,9 +2209,12 @@ propertynotify(XEvent *e) Window trans; XPropertyEvent *ev = &e->xproperty; - if ((ev->window == root) && (ev->atom == XA_WM_NAME)) - updatestatus(); - else if (ev->state == PropertyDelete) + if (ev->window == root) { + if (ev->atom == XA_WM_NAME) + updatestatus(); + else + drawbars(); + } else if (ev->state == PropertyDelete) return; /* ignore */ else if ((c = wintoclient(ev->window))) { switch(ev->atom) { @@ -2902,6 +2944,21 @@ updatebars(void) }; XClassHint ch = {"dwm", "dwm"}; for (m = mons; m; m = m->next) { + if (!m->softwin) { + XSetWindowAttributes softwa = { + .override_redirect = True, + .background_pixmap = ParentRelative, + .event_mask = ExposureMask + }; + int sh_bar = 26; + m->softwin = XCreateWindow(dpy, root, m->wx, m->my + m->mh - sh_bar, m->ww, sh_bar, 0, + DefaultDepth(dpy, screen), CopyFromParent, DefaultVisual(dpy, screen), + CWOverrideRedirect|CWBackPixmap|CWEventMask, &softwa); + XDefineCursor(dpy, m->softwin, cursor[CurNormal]->cursor); + XMapWindow(dpy, m->softwin); + XLowerWindow(dpy, m->softwin); + XSetClassHint(dpy, m->softwin, &ch); + } if (m->barwin) continue; m->barwin = XCreateWindow(dpy, root, m->wx, m->by, m->ww, bh, 0, DefaultDepth(dpy, screen), @@ -3266,7 +3323,7 @@ wintomon(Window w) if (w == root && getrootptr(&x, &y)) return recttomon(x, y, 1, 1); for (m = mons; m; m = m->next) - if (w == m->barwin) + if (w == m->barwin || w == m->softwin) return m; if ((c = wintoclient(w))) return c->mon; diff --git a/src/xdrawer/xdrawer.cpp b/src/xdrawer/xdrawer.cpp index 8a11fe6..5dc6bc6 100644 --- a/src/xdrawer/xdrawer.cpp +++ b/src/xdrawer/xdrawer.cpp @@ -703,6 +703,7 @@ static int xdrawer_run_main(int argc, char** argv) { } closeOtherPlugin("xdialer.pid", "/tmp/dwm_plugin_dialer.fifo"); closeOtherPlugin("xcalllog.pid", "/tmp/dwm_plugin_calllog.fifo"); + closeOtherPlugin("xcontacts.pid", "/tmp/dwm_plugin_contacts.fifo"); openStartMs = nowMs(); startupIgnoreUntilMs = openStartMs + 180; initialKeysReleased = (querySoftkeysDown(dpy) == 0);