#include "phone_data.h" #include "../sxmo-dwm/dwm_plugin.h" #include #include #include #include #include #include #include #include #include namespace { using namespace bananarchy; volatile sig_atomic_t gRunning = 1; void handleSignal(int) { gRunning = 0; } enum class ViewMode { Threads = 0, ThreadView = 1, Compose = 2, PickContact = 3, Options = 4, ConfirmDelete = 5, }; enum class TextCaseMode { Title = 0, // Abc Upper = 1, // ABC Lower = 2, // abc Num = 3, // 123 }; const char* kMultiTapSms[10] = { " 0\n", // 0 ".,?!'-:;()@/1", // 1 "abc2", // 2 "def3", // 3 "ghi4", // 4 "jkl5", // 5 "mno6", // 6 "pqrs7", // 7 "tuv8", // 8 "wxyz9" // 9 }; void popUtf8Char(std::string& s) { if (s.empty()) { return; } while (!s.empty() && (static_cast(s.back()) & 0xC0) == 0x80) { s.pop_back(); } if (!s.empty()) { s.pop_back(); } } int runSmsApp(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], "sendtextmenu") == 0 || std::strcmp(argv[i], "conversationloop") == 0) { if (i + 1 < argc && argv[i + 1]) { initArg = std::string("send:") + argv[++i]; } else { initArg = "send:"; } } else if (std::strcmp(argv[i], "tailtextlog") == 0) { if (i + 1 < argc && argv[i + 1]) { initArg = std::string("thread:") + argv[++i]; } } else if ( std::strcmp(argv[i], "readtextmenu") == 0 ) { initArg.clear(); } else if (initArg.empty()) { initArg = argv[i]; } } std::string daemonPidPath = getPluginDaemonPidPath("sms"); std::string activePidPath = getRuntimeFile("xsms.pid"); if (isPluginDaemonAlive("sms")) { if (!isPreload) { sendPluginCommand("sms", initArg.empty() ? "OPEN" : ("OPEN " + initArg)); } return 0; } writeIntFile(daemonPidPath, static_cast(::getpid())); int fifoFd = setupPluginDaemonFifo("sms"); int notifyPipe[2] = {-1, -1}; if (::pipe(notifyPipe) == 0) { ::fcntl(notifyPipe[0], F_SETFL, O_NONBLOCK | O_CLOEXEC); ::fcntl(notifyPipe[1], F_SETFL, O_NONBLOCK | O_CLOEXEC); } 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); if (notifyPipe[0] >= 0) ::close(notifyPipe[0]); if (notifyPipe[1] >= 0) ::close(notifyPipe[1]); ::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); ThemeSpec initSpec = loadThemeSpec(dpy, screen); XSetWindowAttributes swa {}; swa.override_redirect = True; swa.background_pixel = initSpec.bgMain; swa.border_pixel = initSpec.selBg; 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[] = "xsms"; char resClass[] = "XSms"; ch.res_name = resName; ch.res_class = resClass; XSetClassHint(dpy, win, &ch); XStoreName(dpy, win, "Messages"); Pixmap pix = XCreatePixmap(dpy, win, kWinWidth, kWinHeight, DefaultDepth(dpy, screen)); GC gc = XCreateGC(dpy, pix, 0, nullptr); XftDraw* xftDraw = XftDrawCreate(dpy, pix, visual, cmap); PluginTheme pt; pt.init(dpy, screen, visual, cmap, win); auto& layoutMain = pt.layoutMain; auto& layoutSmall = pt.layoutSmall; auto& layoutIcon = pt.layoutIcon; auto& colWhite = pt.colWhite; auto& colLight = pt.colLight; auto& colMuted = pt.colMuted; auto& colYellow = pt.colYellow; auto& colGreen = pt.colGreen; auto& colBlue = pt.colBlue; auto& colRed = pt.colRed; ContactBook book; std::vector allThreads; std::vector filteredThreads; std::vector threadMessages; std::vector pickerContacts; FileStamp contactsStamp; FileStamp modemLogStamp; bool loadedOnce = false; ViewMode mode = ViewMode::Threads; ViewMode prevMode = ViewMode::Threads; std::string searchQuery; int selectedThread = 0; int selectedMsg = 0; int selectedOption = 0; int selectedPicker = 0; int confirmSelected = 0; std::string activeNumber; std::string composeRecipient; std::string composeText; int composeField = 1; // 0 = To, 1 = Message body, 2 = Send button, 3 = Save Draft TextCaseMode caseMode = TextCaseMode::Title; int lastTapDigit = -1; int lastTapCycle = 0; int64_t lastTapMs = 0; bool zeroHeld = false; int64_t zeroPressMs = 0; std::mutex sendMutex; bool sendingInProgress = false; std::string statusBanner; int64_t statusBannerUntilMs = 0; bool visible = false; int64_t openStartMs = 0; int64_t startupIgnoreUntilMs = 0; bool initialSoftkeysReleased = true; int softkeyPending = 0; int64_t softkeyDeadlineMs = 0; struct OptionEntry { int id; std::string icon; std::string label; }; std::vector optionsList; auto updateFilter = [&]() { filteredThreads.clear(); if (searchQuery.empty()) { filteredThreads = allThreads; } else { std::string qNorm = normalizePhone(searchQuery); std::string qLower = toLowerStr(searchQuery); for (const auto& t : allThreads) { bool match = (!qLower.empty() && t.lowerName.find(qLower) != std::string::npos) || (!qNorm.empty() && t.t9Name.find(qNorm) != std::string::npos) || (!qNorm.empty() && t.normNumber.find(qNorm) != std::string::npos) || (!qLower.empty() && toLowerStr(t.lastPreview).find(qLower) != std::string::npos); if (match) { filteredThreads.push_back(t); } } } if (selectedThread >= static_cast(filteredThreads.size())) { selectedThread = std::max(0, static_cast(filteredThreads.size()) - 1); } }; auto reloadAllData = [&](bool force = false) { bool cChanged = contactsStamp.checkAndUpdate(getContactsFilePath()); bool lChanged = modemLogStamp.checkAndUpdate(getModemLogFilePath()); if (force || cChanged || lChanged || !loadedOnce) { loadedOnce = true; book.load(); allThreads = loadSmsThreads(book); updateFilter(); if (!activeNumber.empty()) { threadMessages = loadSmsThreadMessages(activeNumber); if (selectedMsg >= static_cast(threadMessages.size())) { selectedMsg = std::max(0, static_cast(threadMessages.size()) - 1); } } } }; reloadAllData(true); auto openThreadView = [&](const std::string& num) { activeNumber = trimStr(num); threadMessages = loadSmsThreadMessages(activeNumber); selectedMsg = std::max(0, static_cast(threadMessages.size()) - 1); mode = ViewMode::ThreadView; }; auto openComposeView = [&](const std::string& num, const std::string& prefill = "") { prevMode = mode; composeRecipient = trimStr(num); if (!prefill.empty()) { composeText = prefill; } else if (!composeRecipient.empty()) { composeText = loadSmsDraft(composeRecipient); } else { composeText.clear(); } composeField = composeRecipient.empty() ? 0 : 1; lastTapDigit = -1; mode = ViewMode::Compose; }; auto buildOptions = [&]() { optionsList.clear(); if (mode == ViewMode::Threads) { optionsList.push_back({1, kIconPlus, "New Message"}); if (!filteredThreads.empty()) { optionsList.push_back({2, kIconEye, "Open Conversation"}); optionsList.push_back({3, kIconSend, "Reply to Selected"}); optionsList.push_back({4, kIconPhone, "Call Contact"}); optionsList.push_back({5, kIconUser, "Open in Contacts"}); optionsList.push_back({6, kIconTrash, "Delete Conversation"}); } } else if (mode == ViewMode::ThreadView) { optionsList.push_back({3, kIconSend, "Reply / Compose"}); optionsList.push_back({4, kIconPhone, "Call Contact"}); if (!threadMessages.empty()) { optionsList.push_back({7, kIconCopy, "Forward Selected Msg"}); } optionsList.push_back({5, kIconUser, "Save / View Contact"}); optionsList.push_back({6, kIconTrash, "Delete Conversation"}); } selectedOption = 0; }; auto startAsyncSendSms = [&](const std::string& rawRecipient, const std::string& body) { std::string recipient = normalizePhone(rawRecipient); if (recipient.empty() || trimStr(body).empty()) { statusBanner = "Set recipient and message"; statusBannerUntilMs = nowMs() + 2000; return; } { std::lock_guard lk(sendMutex); if (sendingInProgress) { return; } sendingInProgress = true; } saveSmsDraft(recipient, body); statusBanner = "Sending SMS to " + recipient + "..."; statusBannerUntilMs = nowMs() + 15000; int writePipe = notifyPipe[1]; std::thread([recipient, body, writePipe, &sendMutex, &sendingInProgress, &statusBanner, &statusBannerUntilMs]() { std::string draftPath = getModemLogDirPath() + "/" + recipient + "/draft.txt"; ContactBook::ensureParentDir(draftPath); { std::ofstream out(draftPath, std::ios::trunc); out << body << "\n"; } int rc = -1; pid_t pid = ::fork(); if (pid == 0) { 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); } execlp("sxmo_modemsendsms.sh", "sxmo_modemsendsms.sh", recipient.c_str(), "-f", draftPath.c_str(), static_cast(nullptr)); execl("/usr/bin/sxmo_modemsendsms.sh", "sxmo_modemsendsms.sh", recipient.c_str(), "-f", draftPath.c_str(), static_cast(nullptr)); _exit(127); } else if (pid > 0) { int status = 0; ::waitpid(pid, &status, 0); if (WIFEXITED(status)) { rc = WEXITSTATUS(status); } } // If modem send failed (e.g. offline/no SIM), still log locally if user // wants local record or keep draft so nothing is lost if (rc == 0) { removeSmsDraft(recipient); } else { // Record in local thread log as Sent (or Draft) so conversation is visible time_t nowSec = std::time(nullptr); struct tm tmNow {}; localtime_r(&nowSec, &tmNow); char tbuf[32]; std::strftime(tbuf, sizeof(tbuf), "%Y-%m-%dT%H:%M:%S%z", &tmNow); std::string smsPath = getModemLogDirPath() + "/" + recipient + "/sms.txt"; ContactBook::ensureParentDir(smsPath); std::ofstream smsOut(smsPath, std::ios::app); if (smsOut.is_open()) { smsOut << "Sent at " << tbuf << ":\n" << body << "\n\n"; } std::ofstream logOut(getModemLogFilePath(), std::ios::app); if (logOut.is_open()) { logOut << tbuf << "\tsent_txt\t" << recipient << "\t" << body.size() << " chars\n"; } removeSmsDraft(recipient); } { std::lock_guard lk(sendMutex); sendingInProgress = false; statusBanner = (rc == 0) ? "SMS Sent!" : "Saved to Sent (modem offline)"; statusBannerUntilMs = nowMs() + 2200; } if (writePipe >= 0) { char c = 'S'; (void)::write(writePipe, &c, 1); } }).detach(); }; 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); } int tw = 0, th = 0; pango_layout_get_pixel_size(layout, &tw, &th); int dy = y + std::max(0, (h - th) / 2); pango_xft_render_layout(xftDraw, color, layout, x * PANGO_SCALE, dy * PANGO_SCALE); return tw; }; auto drawSoftkeyBar = [&](const std::string& leftLabel, const std::string& centerLabel, const std::string& rightLabel) { const int barY = kWinHeight - 26; XSetForeground(dpy, gc, pt.spec.bgFooter); XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 26); XSetForeground(dpy, gc, pt.spec.divider); XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY); drawPango(layoutMain, &colBlue, 6, barY, 26, leftLabel); pango_layout_set_width(layoutMain, -1); pango_layout_set_text(layoutMain, centerLabel.c_str(), static_cast(centerLabel.size())); int cw = 0, chh = 0; pango_layout_get_pixel_size(layoutMain, &cw, &chh); drawPango(layoutMain, &colWhite, (kWinWidth - cw) / 2, barY, 26, centerLabel); pango_layout_set_width(layoutMain, -1); pango_layout_set_text(layoutMain, rightLabel.c_str(), static_cast(rightLabel.size())); int rw = 0, rhh = 0; pango_layout_get_pixel_size(layoutMain, &rw, &rhh); drawPango(layoutMain, &colLight, kWinWidth - rw - 6, barY, 26, rightLabel); }; auto drawUI = [&]() { if (!visible) return; pt.checkAndReload(); XSetForeground(dpy, gc, pt.spec.bgMain); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); if (mode == ViewMode::Threads) { XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 38); XSetForeground(dpy, gc, pt.spec.border); XDrawLine(dpy, pix, gc, 0, 37, kWinWidth, 37); if (searchQuery.empty()) { drawPango(layoutIcon, &colYellow, 8, 2, 34, kIconSms); drawPango(layoutMain, &colWhite, 30, 2, 34, "Messages", 145); std::string cnt = "[" + std::to_string(allThreads.size()) + "]"; pango_layout_set_width(layoutSmall, -1); pango_layout_set_text(layoutSmall, cnt.c_str(), static_cast(cnt.size())); int cw = 0, chh = 0; pango_layout_get_pixel_size(layoutSmall, &cw, &chh); drawPango(layoutSmall, &colMuted, kWinWidth - cw - 8, 2, 34, cnt); } else { drawPango(layoutIcon, &colBlue, 8, 2, 34, kIconSearch); drawPango(layoutMain, &colYellow, 30, 2, 34, searchQuery, 155); std::string cnt = std::to_string(filteredThreads.size()); pango_layout_set_width(layoutSmall, -1); pango_layout_set_text(layoutSmall, cnt.c_str(), static_cast(cnt.size())); int cw = 0, chh = 0; pango_layout_get_pixel_size(layoutSmall, &cw, &chh); drawPango(layoutSmall, &colMuted, kWinWidth - cw - 8, 2, 34, cnt); } const int listY = 38; const int rowH = 38; const int visibleRows = 6; if (filteredThreads.empty()) { drawPango(layoutMain, &colMuted, 16, 110, 24, searchQuery.empty() ? "No messages yet" : "No matches", 208); drawPango(layoutSmall, &colBlue, 16, 138, 20, "Press Left Softkey for New SMS", 208); } else { int scroll = 0; if (selectedThread >= visibleRows) { scroll = selectedThread - visibleRows + 1; } for (int i = 0; i < visibleRows; ++i) { int idx = scroll + i; if (idx >= static_cast(filteredThreads.size())) break; const auto& th = filteredThreads[idx]; int ry = listY + i * rowH; bool isSel = (idx == selectedThread); if (isSel) { XSetForeground(dpy, gc, pt.spec.selBg); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); XSetForeground(dpy, gc, pt.spec.accentAlt); XDrawLine(dpy, pix, gc, 0, ry, kWinWidth, ry); } else { XSetForeground(dpy, gc, (i % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); } const char* ic = th.hasDraft ? kIconEdit : (th.lastIncoming ? kIconInCall : kIconOutCall); XftColor* icCol = th.hasDraft ? &colYellow : (th.lastIncoming ? &colGreen : &colBlue); drawPango(layoutIcon, icCol, 6, ry + 1, 20, ic); std::string title = th.contactName.empty() ? th.number : th.contactName; drawPango(layoutMain, &colWhite, 26, ry + 1, 20, title, 128); if (!th.lastDisplayTime.empty()) { pango_layout_set_width(layoutSmall, -1); pango_layout_set_text(layoutSmall, th.lastDisplayTime.c_str(), static_cast(th.lastDisplayTime.size())); int tw = 0, thh = 0; pango_layout_get_pixel_size(layoutSmall, &tw, &thh); drawPango(layoutSmall, isSel ? &colLight : &colMuted, kWinWidth - tw - 6, ry + 2, 18, th.lastDisplayTime); } drawPango(layoutSmall, isSel ? &colLight : &colMuted, 26, ry + 20, 16, th.lastPreview, 206); } } drawSoftkeyBar("Options", filteredThreads.empty() ? "New SMS" : "Open", searchQuery.empty() ? "Close" : "Clear"); } else if (mode == ViewMode::ThreadView) { XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36); XSetForeground(dpy, gc, pt.spec.border); XDrawLine(dpy, pix, gc, 0, 35, kWinWidth, 35); std::string cname = book.resolveName(activeNumber); std::string title = cname.empty() ? activeNumber : cname; drawPango(layoutIcon, &colYellow, 6, 1, 18, kIconSms); drawPango(layoutMain, &colWhite, 26, 0, 20, title, 160); std::string sub = cname.empty() ? (std::to_string(threadMessages.size()) + " msgs") : activeNumber; drawPango(layoutSmall, &colMuted, 26, 19, 15, sub, 204); if (threadMessages.empty()) { drawPango(layoutMain, &colMuted, 16, 110, 24, "No messages in thread", 208); drawPango(layoutSmall, &colBlue, 16, 138, 20, "Press OK to write a message", 208); } else { // Measure and render message cards from bottom up or around selectedMsg const int areaTop = 38; const int areaBot = kWinHeight - 28; const int areaH = areaBot - areaTop; struct CardGeom { int idx; int h; std::string header; std::string body; bool incoming; }; std::vector cards; cards.reserve(threadMessages.size()); for (size_t i = 0; i < threadMessages.size(); ++i) { const auto& m = threadMessages[i]; CardGeom cg; cg.idx = static_cast(i); cg.incoming = m.incoming; cg.header = (m.incoming ? "Recv " : "Sent ") + m.displayTime; cg.body = m.text; pango_layout_set_width(layoutSmall, 204 * PANGO_SCALE); pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR); pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE); pango_layout_set_text(layoutSmall, cg.body.c_str(), static_cast(cg.body.size())); int bw = 0, bh = 0; pango_layout_get_pixel_size(layoutSmall, &bw, &bh); cg.h = std::max(38, bh + 22); cards.push_back(cg); } // Determine visible slice ending at selectedMsg (or covering selectedMsg) int endIdx = std::clamp(selectedMsg, 0, static_cast(cards.size()) - 1); int startIdx = endIdx; int usedH = cards[endIdx].h; while (startIdx > 0 && usedH + cards[startIdx - 1].h + 4 <= areaH) { --startIdx; usedH += cards[startIdx].h + 4; } while (endIdx + 1 < static_cast(cards.size()) && usedH + cards[endIdx + 1].h + 4 <= areaH) { ++endIdx; usedH += cards[endIdx].h + 4; } int curY = areaTop + 2; for (int i = startIdx; i <= endIdx; ++i) { const auto& cg = cards[i]; bool isSel = (cg.idx == selectedMsg); int cardX = cg.incoming ? 4 : 14; int cardW = kWinWidth - 18; int cardH = std::min(cg.h, areaH - 4); unsigned long bg = isSel ? pt.spec.bgInputFocus : (cg.incoming ? pt.spec.bgBubbleIn : pt.spec.bgBubbleOut); XSetForeground(dpy, gc, bg); XFillRectangle(dpy, pix, gc, cardX, curY, cardW, cardH); unsigned long accent = cg.incoming ? pt.spec.success : pt.spec.info; XSetForeground(dpy, gc, isSel ? pt.spec.accent : accent); if (cg.incoming) { XFillRectangle(dpy, pix, gc, cardX, curY, 3, cardH); } else { XFillRectangle(dpy, pix, gc, cardX + cardW - 3, curY, 3, cardH); } drawPango(layoutSmall, cg.incoming ? &colGreen : &colBlue, cardX + 8, curY + 2, 14, cg.header, cardW - 14); pango_layout_set_width(layoutSmall, (cardW - 14) * PANGO_SCALE); pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR); pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE); pango_layout_set_text(layoutSmall, cg.body.c_str(), static_cast(cg.body.size())); pango_xft_render_layout(xftDraw, &colWhite, layoutSmall, (cardX + 8) * PANGO_SCALE, (curY + 18) * PANGO_SCALE); curY += cardH + 4; } } drawSoftkeyBar("Options", "Reply", "Back"); } else if (mode == ViewMode::Compose) { XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 32); XSetForeground(dpy, gc, pt.spec.border); XDrawLine(dpy, pix, gc, 0, 31, kWinWidth, 31); drawPango(layoutIcon, &colBlue, 6, 0, 30, kIconSend); drawPango(layoutMain, &colWhite, 26, 0, 30, "Compose", 100); const char* modeBadge = "[Abc]"; if (composeField == 0) { modeBadge = "[123]"; } else if (caseMode == TextCaseMode::Upper) { modeBadge = "[ABC]"; } else if (caseMode == TextCaseMode::Lower) { modeBadge = "[abc]"; } else if (caseMode == TextCaseMode::Num) { modeBadge = "[123]"; } int segs = std::max(1, static_cast(composeText.size() + 159) / 160); std::string rightHdr = std::string(modeBadge) + " " + std::to_string(composeText.size()) + "/" + std::to_string(segs * 160); pango_layout_set_width(layoutSmall, -1); pango_layout_set_text(layoutSmall, rightHdr.c_str(), static_cast(rightHdr.size())); int rw = 0, rhh = 0; pango_layout_get_pixel_size(layoutSmall, &rw, &rhh); drawPango(layoutSmall, &colYellow, kWinWidth - rw - 6, 0, 30, rightHdr); // Field 0: Recipient bool toSel = (composeField == 0); XSetForeground(dpy, gc, toSel ? pt.spec.bgInputFocus : pt.spec.bgInput); XFillRectangle(dpy, pix, gc, 6, 36, kWinWidth - 12, 32); XSetForeground(dpy, gc, toSel ? pt.spec.accentAlt : pt.spec.border); XDrawRectangle(dpy, pix, gc, 6, 36, kWinWidth - 13, 31); std::string cname = book.resolveName(composeRecipient); std::string toDisp = "To: " + composeRecipient; if (toSel) toDisp += "|"; if (!cname.empty()) { toDisp += " (" + cname + ")"; } drawPango(layoutMain, toSel ? &colWhite : &colLight, 10, 36, 32, toDisp, kWinWidth - 20); // Field 1: Message Body bool msgSel = (composeField == 1); XSetForeground(dpy, gc, msgSel ? pt.spec.bgInputFocus : pt.spec.bgInput); XFillRectangle(dpy, pix, gc, 6, 72, kWinWidth - 12, 156); XSetForeground(dpy, gc, msgSel ? pt.spec.accentAlt : pt.spec.border); XDrawRectangle(dpy, pix, gc, 6, 72, kWinWidth - 13, 155); std::string bodyDisp = composeText; if (msgSel) bodyDisp += "|"; if (bodyDisp.empty()) { drawPango(layoutSmall, &colMuted, 12, 78, 20, "Type message (# Input mode)...", kWinWidth - 24); } else { pango_layout_set_width(layoutMain, (kWinWidth - 24) * PANGO_SCALE); pango_layout_set_wrap(layoutMain, PANGO_WRAP_WORD_CHAR); pango_layout_set_ellipsize(layoutMain, PANGO_ELLIPSIZE_NONE); pango_layout_set_text(layoutMain, bodyDisp.c_str(), static_cast(bodyDisp.size())); int tw = 0, th = 0; pango_layout_get_pixel_size(layoutMain, &tw, &th); int textY = 76; if (th > 146) { textY = 76 - (th - 146); } XRectangle clipRect {6, 73, static_cast(kWinWidth - 12), 154}; XftDrawSetClipRectangles(xftDraw, 0, 0, &clipRect, 1); pango_xft_render_layout(xftDraw, &colWhite, layoutMain, 12 * PANGO_SCALE, textY * PANGO_SCALE); XftDrawSetClip(xftDraw, nullptr); } // Field 2 & 3: Send & Save Draft buttons bool sendSel = (composeField == 2); bool draftSel = (composeField == 3); XSetForeground(dpy, gc, sendSel ? pt.spec.selBg : pt.spec.bgButton); XFillRectangle(dpy, pix, gc, 6, 234, 118, 30); XSetForeground(dpy, gc, sendSel ? pt.spec.accentAlt : pt.spec.border); XDrawRectangle(dpy, pix, gc, 6, 234, 117, 29); drawPango(layoutMain, &colGreen, 18, 234, 30, "Send SMS", 100); XSetForeground(dpy, gc, draftSel ? pt.spec.selBg : pt.spec.bgButton); XFillRectangle(dpy, pix, gc, 130, 234, 104, 30); XSetForeground(dpy, gc, draftSel ? pt.spec.accentAlt : pt.spec.border); XDrawRectangle(dpy, pix, gc, 130, 234, 103, 29); drawPango(layoutMain, &colYellow, 146, 234, 30, "Draft", 84); if (composeField == 0) { drawSoftkeyBar("Contacts", composeRecipient.empty() ? "Contacts" : "Next", composeRecipient.empty() ? "Cancel" : "Clear"); } else if (composeField == 1) { drawSoftkeyBar("Send", "Send", composeText.empty() ? "Cancel" : "Del"); } else if (composeField == 2) { drawSoftkeyBar("Send", "Send", "Cancel"); } else { drawSoftkeyBar("Send", "Draft", "Cancel"); } } else if (mode == ViewMode::PickContact) { XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 34); drawPango(layoutIcon, &colBlue, 8, 0, 34, kIconBook); drawPango(layoutMain, &colWhite, 30, 0, 34, "Select Recipient", 200); const int listY = 36; const int rowH = 38; const int visibleRows = 6; if (pickerContacts.empty()) { drawPango(layoutMain, &colMuted, 16, 110, 24, "No saved contacts", 208); } else { int scroll = 0; if (selectedPicker >= visibleRows) { scroll = selectedPicker - visibleRows + 1; } for (int i = 0; i < visibleRows; ++i) { int idx = scroll + i; if (idx >= static_cast(pickerContacts.size())) break; const auto& c = pickerContacts[idx]; int ry = listY + i * rowH; bool isSel = (idx == selectedPicker); XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgMain); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); drawPango(layoutIcon, c.isRecent ? &colYellow : &colBlue, 6, ry + 1, 20, c.isRecent ? kIconClock : kIconUser); drawPango(layoutMain, &colWhite, 28, ry + 1, 20, c.name, 204); drawPango(layoutSmall, isSel ? &colLight : &colMuted, 28, ry + 20, 16, c.number, 204); } } drawSoftkeyBar("Select", "Select", "Back"); } else if (mode == ViewMode::Options) { XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 34); drawPango(layoutIcon, &colBlue, 8, 0, 34, kIconList); drawPango(layoutMain, &colWhite, 30, 0, 34, "Message Options", 200); const int listY = 38; const int rowH = 36; for (size_t i = 0; i < optionsList.size(); ++i) { int ry = listY + static_cast(i) * rowH; bool isSel = (static_cast(i) == selectedOption); XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgMain); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); drawPango(layoutIcon, isSel ? &colYellow : &colBlue, 10, ry, rowH, optionsList[i].icon); drawPango(layoutMain, &colWhite, 34, ry, rowH, optionsList[i].label, 198); } drawSoftkeyBar("Select", "Select", "Back"); } else if (mode == ViewMode::ConfirmDelete) { XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36); drawPango(layoutIcon, &colRed, 8, 0, 36, kIconTrash); drawPango(layoutMain, &colWhite, 30, 0, 36, "Delete Thread?", 200); drawPango(layoutMain, &colWhite, 14, 70, 26, activeNumber, 212); drawPango(layoutSmall, &colMuted, 14, 98, 22, "All SMS history for this number", 212); drawPango(layoutSmall, &colMuted, 14, 118, 22, "will be removed.", 212); for (int i = 0; i < 2; ++i) { int ry = 164 + i * 40; bool isSel = (confirmSelected == i); XSetForeground( dpy, gc, isSel ? (i == 1 ? pt.spec.dangerBg : pt.spec.selBg) : pt.spec.bgButton); XFillRectangle(dpy, pix, gc, 16, ry, kWinWidth - 32, 34); drawPango(layoutMain, &colWhite, 28, ry, 34, i == 0 ? "No, Keep Thread" : "Yes, Delete Thread", 184); } drawSoftkeyBar("Select", "Confirm", "Cancel"); } int64_t tNow = nowMs(); { std::lock_guard lk(sendMutex); if (!statusBanner.empty() && tNow < statusBannerUntilMs) { XSetForeground(dpy, gc, pt.spec.bgToast); XFillRectangle(dpy, pix, gc, 8, kWinHeight - 58, kWinWidth - 16, 28); XSetForeground(dpy, gc, pt.spec.info); XDrawRectangle(dpy, pix, gc, 8, kWinHeight - 58, kWinWidth - 17, 27); drawPango(layoutSmall, &colWhite, 14, kWinHeight - 58, 28, statusBanner, kWinWidth - 28); } } XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); XFlush(dpy); }; auto showWindow = [&](const std::string& argStr) { writeIntFile(activePidPath, static_cast(::getpid())); closeOtherPlugins("sms"); reloadAllData(true); lastTapDigit = -1; zeroHeld = false; softkeyPending = 0; if (argStr.rfind("send:", 0) == 0) { openComposeView(argStr.substr(5)); } else if (argStr.rfind("thread:", 0) == 0) { openThreadView(argStr.substr(7)); } else if (!argStr.empty()) { openThreadView(argStr); } else { mode = ViewMode::Threads; searchQuery.clear(); updateFilter(); selectedThread = 0; } if (!visible) { visible = true; XMapRaised(dpy, win); } else { XRaiseWindow(dpy, win); } grabPluginKeyboard(dpy, win); openStartMs = nowMs(); startupIgnoreUntilMs = openStartMs + 180; initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); drawUI(); }; auto hideWindow = [&]() { if (!visible) return; if (mode == ViewMode::Compose && !composeRecipient.empty()) { saveSmsDraft(normalizePhone(composeRecipient), composeText); } writeSoftkeySuppress(nowMs() + 450); XUngrabKeyboard(dpy, CurrentTime); XUnmapWindow(dpy, win); XFlush(dpy); visible = false; ::unlink(activePidPath.c_str()); }; auto commitMultiTapChar = [&](int digit) { const char* chars = kMultiTapSms[digit]; int len = static_cast(std::strlen(chars)); if (len <= 0) return; int64_t t = nowMs(); if (caseMode == TextCaseMode::Num) { composeText.push_back(static_cast('0' + digit)); lastTapDigit = -1; return; } bool sameKey = (lastTapDigit == digit && (t - lastTapMs) <= 850 && !composeText.empty()); if (sameKey) { lastTapCycle = (lastTapCycle + 1) % len; popUtf8Char(composeText); } else { lastTapDigit = digit; lastTapCycle = 0; } lastTapMs = t; char ch = chars[lastTapCycle]; if (std::isalpha(static_cast(ch))) { if (caseMode == TextCaseMode::Upper) { ch = static_cast(std::toupper(static_cast(ch))); } else if (caseMode == TextCaseMode::Title) { bool startOfWord = composeText.empty() || composeText.back() == ' ' || composeText.back() == '\n' || composeText.back() == '.'; if (startOfWord) { ch = static_cast(std::toupper(static_cast(ch))); } } } composeText.push_back(ch); }; auto executeOption = [&](int optId) { if (optId == 1) { openComposeView(""); } else if (optId == 2 && !filteredThreads.empty()) { openThreadView(filteredThreads[selectedThread].number); } else if (optId == 3) { std::string target = (prevMode == ViewMode::ThreadView) ? activeNumber : (!filteredThreads.empty() ? filteredThreads[selectedThread].number : ""); openComposeView(target); } else if (optId == 4) { std::string target = (prevMode == ViewMode::ThreadView) ? activeNumber : (!filteredThreads.empty() ? filteredThreads[selectedThread].number : ""); if (!target.empty()) { hideWindow(); placePhoneCall(target); } } else if (optId == 5) { std::string target = (prevMode == ViewMode::ThreadView) ? activeNumber : (!filteredThreads.empty() ? filteredThreads[selectedThread].number : ""); hideWindow(); if (!target.empty() && book.resolveName(target).empty()) { sendPluginCommand("contacts", "OPEN new:" + target); } else { sendPluginCommand("contacts", "OPEN " + target); } } else if (optId == 6) { activeNumber = (prevMode == ViewMode::ThreadView) ? activeNumber : (!filteredThreads.empty() ? filteredThreads[selectedThread].number : ""); if (!activeNumber.empty()) { confirmSelected = 0; mode = ViewMode::ConfirmDelete; } else { mode = prevMode; } } else if (optId == 7 && !threadMessages.empty()) { std::string body = threadMessages[selectedMsg].text; openComposeView("", body); } }; auto handleSoftkeyAction = [&](bool isLeft) { if (mode == ViewMode::Threads) { if (isLeft) { prevMode = ViewMode::Threads; buildOptions(); mode = ViewMode::Options; } else { if (!searchQuery.empty()) { popUtf8Char(searchQuery); updateFilter(); } else { hideWindow(); return; } } } else if (mode == ViewMode::ThreadView) { if (isLeft) { prevMode = ViewMode::ThreadView; buildOptions(); mode = ViewMode::Options; } else { reloadAllData(true); mode = ViewMode::Threads; } } else if (mode == ViewMode::Compose) { if (isLeft) { if (composeField == 0) { pickerContacts = book.buildDialerList(); selectedPicker = 0; mode = ViewMode::PickContact; } else { startAsyncSendSms(composeRecipient, composeText); } } else { if (composeField == 0 && !composeRecipient.empty()) { popUtf8Char(composeRecipient); } else if (composeField == 1 && !composeText.empty()) { popUtf8Char(composeText); lastTapDigit = -1; } else { if (!composeRecipient.empty()) { saveSmsDraft(normalizePhone(composeRecipient), composeText); } reloadAllData(true); mode = (prevMode == ViewMode::ThreadView) ? ViewMode::ThreadView : ViewMode::Threads; } } } else if (mode == ViewMode::PickContact) { if (isLeft) { if (!pickerContacts.empty()) { composeRecipient = pickerContacts[selectedPicker].number; composeField = 1; } mode = ViewMode::Compose; } else { mode = ViewMode::Compose; } } else if (mode == ViewMode::Options) { if (isLeft) { if (!optionsList.empty()) { executeOption(optionsList[selectedOption].id); } } else { mode = prevMode; } } else if (mode == ViewMode::ConfirmDelete) { if (isLeft) { if (confirmSelected == 1) { deleteSmsThread(activeNumber); reloadAllData(true); mode = ViewMode::Threads; } else { mode = prevMode; } } else { mode = prevMode; } } drawUI(); }; if (!isPreload) { showWindow(initArg); } int xfd = ConnectionNumber(dpy); while (gRunning) { for (const auto& cmd : readFifoLines(fifoFd)) { if (cmd == "NOP") { continue; } else if (cmd == "RELOAD_THEME") { pt.reload(); if (visible) drawUI(); } else if (cmd == "CLOSE") { hideWindow(); } else if (cmd == "OPEN") { showWindow(""); } else if (cmd.rfind("OPEN ", 0) == 0) { showWindow(trimStr(cmd.substr(5))); } } while (XPending(dpy) > 0) { XEvent ev; XNextEvent(dpy, &ev); if (!visible) continue; if (ev.type == Expose && ev.xexpose.count == 0) { drawUI(); } else if (ev.type == VisibilityNotify && ev.xvisibility.state != VisibilityUnobscured) { if (isAnotherPluginActive("sms")) { hideWindow(); } else { XRaiseWindow(dpy, win); grabPluginKeyboard(dpy, win); drawUI(); } } else if (ev.type == KeyRelease) { unsigned int kc = ev.xkey.keycode; KeySym sym = XLookupKeysym(&ev.xkey, 0); if (kc == 19 || sym == XK_0 || sym == XK_KP_0) { zeroHeld = false; } } else if (ev.type == KeyPress) { int64_t tNow = nowMs(); unsigned int kc = ev.xkey.keycode; KeySym sym = XLookupKeysym(&ev.xkey, 0); bool isLeftSk = (kc == 135 || sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 || sym == _EVDEVK(0x246) || sym == XK_F1); bool isRightSk = (kc == 166 || sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 || sym == _EVDEVK(0x247) || sym == XK_F2); if (!initialSoftkeysReleased) { if (querySoftkeysDown(dpy) == 0) { initialSoftkeysReleased = true; } else if (isLeftSk || isRightSk) { continue; } } if (tNow < startupIgnoreUntilMs && (isLeftSk || isRightSk)) { continue; } if (isLeftSk || isRightSk) { int bit = isLeftSk ? 1 : 2; softkeyPending |= bit; if (softkeyPending == 3 || querySoftkeysDown(dpy) == 3) { softkeyPending = 0; sendPluginCommand("xdrawer", "TOGGLE"); continue; } softkeyDeadlineMs = tNow + 35; continue; } if (sym == XK_Escape || kc == 124 || sym == XF86XK_PowerOff) { hideWindow(); continue; } bool isCallKey = (kc == 177 || sym == XF86XK_PickupPhone || sym == XF86XK_Phone || sym == _EVDEVK(0x1bd) || sym == _EVDEVK(0x0a9)); if (isCallKey) { if (mode == ViewMode::Compose) { startAsyncSendSms(composeRecipient, composeText); drawUI(); } else if (mode == ViewMode::ThreadView && !activeNumber.empty()) { hideWindow(); placePhoneCall(activeNumber); } else if (mode == ViewMode::Threads && !filteredThreads.empty()) { std::string num = filteredThreads[selectedThread].number; hideWindow(); placePhoneCall(num); } continue; } if (sym == XK_BackSpace || sym == XK_Delete) { handleSoftkeyAction(false); continue; } if (mode == ViewMode::Threads) { if (sym == XK_Up || sym == XK_KP_Up) { if (!filteredThreads.empty()) { selectedThread = (selectedThread - 1 + static_cast(filteredThreads.size())) % static_cast(filteredThreads.size()); drawUI(); } } else if (sym == XK_Down || sym == XK_KP_Down) { if (!filteredThreads.empty()) { selectedThread = (selectedThread + 1) % static_cast(filteredThreads.size()); drawUI(); } } else if (sym == XK_Return || sym == XK_KP_Enter) { if (filteredThreads.empty()) { openComposeView(""); } else { openThreadView(filteredThreads[selectedThread].number); } drawUI(); } else { char d = mapDialKey(sym, kc, ev.xkey.state); if (d >= '0' && d <= '9') { searchQuery.push_back(d); updateFilter(); drawUI(); } else if (d == '*') { openComposeView(""); drawUI(); } } } else if (mode == ViewMode::ThreadView) { if (sym == XK_Up || sym == XK_KP_Up) { if (selectedMsg > 0) { --selectedMsg; drawUI(); } } else if (sym == XK_Down || sym == XK_KP_Down) { if (selectedMsg + 1 < static_cast(threadMessages.size())) { ++selectedMsg; drawUI(); } } else if (sym == XK_Return || sym == XK_KP_Enter) { openComposeView(activeNumber); drawUI(); } } else if (mode == ViewMode::Compose) { if (sym == XK_Up || sym == XK_KP_Up) { lastTapDigit = -1; composeField = std::max(0, composeField - 1); drawUI(); } else if (sym == XK_Down || sym == XK_KP_Down) { lastTapDigit = -1; composeField = std::min(3, composeField + 1); drawUI(); } else if (sym == XK_Left || sym == XK_KP_Left) { if (composeField == 3) { composeField = 2; drawUI(); } else { lastTapDigit = -1; } } else if (sym == XK_Right || sym == XK_KP_Right) { if (composeField == 2) { composeField = 3; drawUI(); } else if (composeField == 0 && composeRecipient.empty()) { composeRecipient = "+"; drawUI(); } else { lastTapDigit = -1; } } else if (sym == XK_Return || sym == XK_KP_Enter) { if (composeField == 0) { if (composeRecipient.empty()) { pickerContacts = book.buildDialerList(); selectedPicker = 0; mode = ViewMode::PickContact; } else { composeField = 1; } } else if (composeField == 1 || composeField == 2) { startAsyncSendSms(composeRecipient, composeText); } else if (composeField == 3) { saveSmsDraft(normalizePhone(composeRecipient), composeText); statusBanner = "Draft saved"; statusBannerUntilMs = nowMs() + 1800; reloadAllData(true); mode = (prevMode == ViewMode::ThreadView) ? ViewMode::ThreadView : ViewMode::Threads; } drawUI(); } else { char d = mapDialKey(sym, kc, ev.xkey.state); if (composeField == 0) { if (d == '0') { zeroHeld = true; zeroPressMs = tNow; composeRecipient.push_back('0'); drawUI(); } else if ((d >= '1' && d <= '9') || d == '+') { composeRecipient.push_back(d); drawUI(); } } else if (composeField == 1) { if (d == '#') { lastTapDigit = -1; caseMode = static_cast( (static_cast(caseMode) + 1) % 4); drawUI(); } else if (d == '*') { commitMultiTapChar(1); drawUI(); } else if (d >= '0' && d <= '9') { commitMultiTapChar(d - '0'); drawUI(); } else if (sym == XK_space) { composeText.push_back(' '); lastTapDigit = -1; drawUI(); } } } } else if (mode == ViewMode::PickContact) { if (sym == XK_Up || sym == XK_KP_Up) { if (!pickerContacts.empty()) { selectedPicker = (selectedPicker - 1 + static_cast(pickerContacts.size())) % static_cast(pickerContacts.size()); drawUI(); } } else if (sym == XK_Down || sym == XK_KP_Down) { if (!pickerContacts.empty()) { selectedPicker = (selectedPicker + 1) % static_cast(pickerContacts.size()); drawUI(); } } else if (sym == XK_Return || sym == XK_KP_Enter) { handleSoftkeyAction(true); } } else if (mode == ViewMode::Options) { if (sym == XK_Up || sym == XK_KP_Up) { if (!optionsList.empty()) { selectedOption = (selectedOption - 1 + static_cast(optionsList.size())) % static_cast(optionsList.size()); drawUI(); } } else if (sym == XK_Down || sym == XK_KP_Down) { if (!optionsList.empty()) { selectedOption = (selectedOption + 1) % static_cast(optionsList.size()); drawUI(); } } else if (sym == XK_Return || sym == XK_KP_Enter) { handleSoftkeyAction(true); } } else if (mode == ViewMode::ConfirmDelete) { if (sym == XK_Up || sym == XK_Down || sym == XK_KP_Up || sym == XK_KP_Down) { confirmSelected = 1 - confirmSelected; drawUI(); } else if (sym == XK_Return || sym == XK_KP_Enter) { handleSoftkeyAction(true); } } } } int64_t tNow = nowMs(); if (visible && softkeyPending != 0 && tNow >= softkeyDeadlineMs) { if (querySoftkeysDown(dpy) == 3) { softkeyPending = 0; sendPluginCommand("xdrawer", "TOGGLE"); } else { bool isLeft = (softkeyPending == 1); softkeyPending = 0; handleSoftkeyAction(isLeft); } } if (visible && zeroHeld && mode == ViewMode::Compose && composeField == 0 && (tNow - zeroPressMs) >= 420) { if (isKeycodeDown(dpy, 19)) { if (!composeRecipient.empty() && composeRecipient.back() == '0') { composeRecipient.back() = '+'; drawUI(); } } zeroHeld = false; } int timeoutMs = -1; if (visible) { timeoutMs = 250; if (softkeyPending != 0) { timeoutMs = std::min(timeoutMs, std::max(1, static_cast(softkeyDeadlineMs - tNow))); } if (zeroHeld) { timeoutMs = std::min( timeoutMs, std::max(1, static_cast((zeroPressMs + 420) - tNow))); } } struct pollfd pfds[3]; int nfds = 0; pfds[nfds++] = {xfd, POLLIN, 0}; int fifoIdx = -1; if (fifoFd >= 0) { fifoIdx = nfds; pfds[nfds++] = {fifoFd, POLLIN, 0}; } int pipeIdx = -1; if (notifyPipe[0] >= 0) { pipeIdx = nfds; pfds[nfds++] = {notifyPipe[0], POLLIN, 0}; } int pr = ::poll(pfds, nfds, timeoutMs); if (pr > 0) { if (fifoIdx >= 0 && (pfds[fifoIdx].revents & POLLIN)) { for (const auto& cmd : readFifoLines(fifoFd)) { if (cmd == "RELOAD_THEME") { pt.reload(); if (visible) drawUI(); } else if (cmd == "CLOSE") { hideWindow(); } else if (cmd == "OPEN") { showWindow(""); } else if (cmd.rfind("OPEN ", 0) == 0) { showWindow(trimStr(cmd.substr(5))); } } } if (pipeIdx >= 0 && (pfds[pipeIdx].revents & POLLIN)) { char drain[32]; while (::read(notifyPipe[0], drain, sizeof(drain)) > 0) { } std::string sentTo = normalizePhone(composeRecipient); reloadAllData(true); if (!sentTo.empty()) { openThreadView(sentTo); } drawUI(); } } else if (pr == 0 && visible) { reloadAllData(false); } } hideWindow(); if (fifoFd >= 0) ::close(fifoFd); if (notifyPipe[0] >= 0) ::close(notifyPipe[0]); if (notifyPipe[1] >= 0) ::close(notifyPipe[1]); ::unlink(daemonPidPath.c_str()); return 0; } int sms_plugin_init(const DwmHostApi* host) { if (host && host->spawn_plugin) { host->spawn_plugin("sms", "--preload"); } return 0; } } // namespace extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { DWM_PLUGIN_API_VERSION, "sms", "Keypad-driven SMS conversation manager and editor integrated with Sxmo", sms_plugin_init, nullptr, nullptr, nullptr, nullptr, runSmsApp };