// SPDX-License-Identifier: GPL-2.0-only // Bananarchy Modem, Dual-SIM & PIN/PUK Manager Plugin (modem.so / xmodem) #include "phone_data.h" #include "../sxmo-dwm/dwm_plugin.h" #include #include #include #include #include #include #include #ifdef None #undef None #endif namespace { using namespace bananarchy; volatile sig_atomic_t gRunning = 1; void handleSignal(int) { gRunning = 0; } enum class TabId { SimCards = 0, RadioInfo = 1, AtConsole = 2, ModemLog = 3, }; enum class ModalMode { None = 0, SimActions = 1, VerifyPin = 2, UnblockPuk = 3, TogglePinLock = 4, ChangePin = 5, UssdDialog = 6, }; struct SimSlotInfo { int slotNum = 1; std::string state = "absent"; // present, absent, error std::string rawState = "absent"; std::string appType = "none"; // usim, sim, none std::string appState = "none"; std::string aid; std::string pin1State = "not-initialized"; int pin1Retries = 0; int puk1Retries = 0; std::string upinState = "not-initialized"; int upinRetries = 0; int upukRetries = 0; bool isActiveProvisioned = false; bool isPreferred = false; bool needsPin() const { return state == "present" && (pin1State == "enabled-not-verified" || appState == "pin1"); } bool needsPuk() const { return state == "present" && (pin1State == "blocked" || appState == "puk1"); } bool isPinEnabled() const { return pin1State == "enabled-not-verified" || pin1State == "enabled-verified" || pin1State == "blocked"; } }; struct ModemStatusData { bool qmiAvailable = false; int preferredSlot = 1; int primaryGwSlot = 0; SimSlotInfo slots[2]; bool mmPresent = false; std::string mmState = "nomodem"; std::string mmFailedReason; std::string mmPowerState = "off"; std::string mmUnlockRequired; int mmPinRetries = -1; int mmPukRetries = -1; std::string operatorName; std::string operatorCode; std::string accessTech; int signalQuality = 0; std::string imei; std::string imei2; std::string imeiSv; std::string ownNumber; std::string simDbusPath; std::string simIccid; std::string simOperatorName; std::string currentModes; int modePresetIdx = 0; }; struct RadioModePreset { const char* label; const char* allowed; const char* preferred; }; constexpr RadioModePreset kRadioModes[] = { {"2G/3G/4G (4G Pref)", "2g|3g|4g", "4g"}, {"2G/3G/4G (3G Pref)", "2g|3g|4g", "3g"}, {"2G/3G (3G Pref)", "2g|3g", "3g"}, {"4G LTE Only", "4g", "none"}, {"3G UMTS Only", "3g", "none"}, {"2G GSM Only", "2g", "none"}, }; constexpr int kNumRadioModes = static_cast(sizeof(kRadioModes) / sizeof(kRadioModes[0])); struct ModemLogRow { std::string timeStr; std::string eventType; std::string target; std::string detail; }; constexpr const char* kAtPresets[] = { "AT", "ATI", "AT+CGSN", "AT+CSQ", "AT+CREG?", "AT+COPS?", "AT+CPIN?", "AT+CCID", "AT+CGDCONT?", "AT+CGPADDR", "AT+CFUN?", "AT+CLCC", }; constexpr int kNumAtPresets = static_cast(sizeof(kAtPresets) / sizeof(kAtPresets[0])); enum class AtTextMode { Upper = 0, // ABC Lower = 1, // abc Num = 2, // 123 Sym = 3, // Sym }; const char* kAtTapMap[10] = { " 0+=", "+?=,1\"':;/-_", "ABC2", "DEF3", "GHI4", "JKL5", "MNO6", "PQRS7", "TUV8", "WXYZ9" }; void parseKvLines(const std::string& text, std::unordered_map& out, char sep) { std::istringstream ss(text); std::string line; while (std::getline(ss, line)) { size_t pos = line.find(sep); if (pos == std::string::npos) continue; std::string k = trimStr(line.substr(0, pos)); std::string v = trimStr(line.substr(pos + 1)); if (!k.empty()) { out[k] = v; } } } ModemStatusData queryModemAndSimStatus() { ModemStatusData d; d.slots[0].slotNum = 1; d.slots[1].slotNum = 2; // 1. Query QMI UIM status for both physical slots via helper std::string uimOut = runCaptureCmd( "doas -n /usr/libexec/msm-modem-uim-selection status 2>/dev/null || " "/usr/libexec/msm-modem-uim-selection status 2>/dev/null"); std::unordered_map uimKv; parseKvLines(uimOut, uimKv, '='); d.qmiAvailable = (uimKv["MODEM_AVAILABLE"] == "1"); if (!uimKv["PREFERRED_SLOT"].empty()) { d.preferredSlot = std::clamp(std::atoi(uimKv["PREFERRED_SLOT"].c_str()), 1, 2); } if (!uimKv["PRIMARY_GW_SLOT"].empty()) { d.primaryGwSlot = std::atoi(uimKv["PRIMARY_GW_SLOT"].c_str()); } for (int i = 0; i < 2; ++i) { std::string pfx = "SLOT" + std::to_string(i + 1) + "_"; auto& s = d.slots[i]; s.slotNum = i + 1; if (!uimKv[pfx + "STATE"].empty()) s.state = uimKv[pfx + "STATE"]; if (!uimKv[pfx + "RAW_STATE"].empty()) s.rawState = uimKv[pfx + "RAW_STATE"]; if (!uimKv[pfx + "APP_TYPE"].empty()) s.appType = uimKv[pfx + "APP_TYPE"]; if (!uimKv[pfx + "APP_STATE"].empty()) s.appState = uimKv[pfx + "APP_STATE"]; s.aid = uimKv[pfx + "AID"]; if (!uimKv[pfx + "PIN1_STATE"].empty()) s.pin1State = uimKv[pfx + "PIN1_STATE"]; if (!uimKv[pfx + "PIN1_RETRIES"].empty()) { s.pin1Retries = std::atoi(uimKv[pfx + "PIN1_RETRIES"].c_str()); } if (!uimKv[pfx + "PUK1_RETRIES"].empty()) { s.puk1Retries = std::atoi(uimKv[pfx + "PUK1_RETRIES"].c_str()); } if (!uimKv[pfx + "UPIN_STATE"].empty()) s.upinState = uimKv[pfx + "UPIN_STATE"]; if (!uimKv[pfx + "UPIN_RETRIES"].empty()) { s.upinRetries = std::atoi(uimKv[pfx + "UPIN_RETRIES"].c_str()); } if (!uimKv[pfx + "UPUK_RETRIES"].empty()) { s.upukRetries = std::atoi(uimKv[pfx + "UPUK_RETRIES"].c_str()); } s.isActiveProvisioned = (d.primaryGwSlot == (i + 1)); s.isPreferred = (d.preferredSlot == (i + 1)); } // 2. Query ModemManager (-m any -K) std::string mmOut = runCaptureCmd("mmcli -m any -K 2>/dev/null"); std::unordered_map mmKv; parseKvLines(mmOut, mmKv, ':'); auto cleanMmVal = [](const std::string& v) -> std::string { return (v == "--") ? "" : v; }; if (!mmKv["modem.dbus-path"].empty()) { d.mmPresent = true; d.mmState = cleanMmVal(mmKv["modem.generic.state"]); d.mmFailedReason = cleanMmVal(mmKv["modem.generic.state-failed-reason"]); d.mmPowerState = cleanMmVal(mmKv["modem.generic.power-state"]); d.mmUnlockRequired = cleanMmVal(mmKv["modem.generic.unlock-required"]); d.operatorName = cleanMmVal(mmKv["modem.3gpp.operator-name"]); d.operatorCode = cleanMmVal(mmKv["modem.3gpp.operator-code"]); d.accessTech = cleanMmVal(mmKv["modem.generic.access-technologies.value[1]"]); if (d.accessTech.empty()) { d.accessTech = cleanMmVal(mmKv["modem.generic.access-technologies"]); } std::string sigStr = cleanMmVal(mmKv["modem.generic.signal-quality.value"]); if (!sigStr.empty()) { d.signalQuality = std::clamp(std::atoi(sigStr.c_str()), 0, 100); } d.imei = cleanMmVal(mmKv["modem.3gpp.imei"]); if (d.imei.empty()) { d.imei = cleanMmVal(mmKv["modem.generic.equipment-identifier"]); } DeviceImeis devImeis = queryDeviceImeis(); if (!devImeis.imei1.empty()) d.imei = devImeis.imei1; if (!devImeis.imei2.empty()) d.imei2 = devImeis.imei2; if (!devImeis.imeiSv.empty()) d.imeiSv = devImeis.imeiSv; if (d.imei2.empty()) d.imei2 = d.imei; d.ownNumber = cleanMmVal(mmKv["modem.generic.own-numbers.value[1]"]); d.simDbusPath = cleanMmVal(mmKv["modem.generic.sim"]); d.currentModes = cleanMmVal(mmKv["modem.generic.current-modes"]); if (d.currentModes.find("allowed: 2g, 3g, 4g; preferred: 4g") != std::string::npos) { d.modePresetIdx = 0; } else if (d.currentModes.find("allowed: 2g, 3g, 4g; preferred: 3g") != std::string::npos) { d.modePresetIdx = 1; } else if (d.currentModes.find("allowed: 2g, 3g; preferred: 3g") != std::string::npos) { d.modePresetIdx = 2; } else if (d.currentModes.find("allowed: 4g") != std::string::npos) { d.modePresetIdx = 3; } else if (d.currentModes.find("allowed: 3g") != std::string::npos) { d.modePresetIdx = 4; } else if (d.currentModes.find("allowed: 2g") != std::string::npos) { d.modePresetIdx = 5; } // Parse unlock-retries if present, e.g. "sim-pin (3), sim-puk (10)" for (int idx = 1; idx <= 6; ++idx) { std::string rk = "modem.generic.unlock-retries.value[" + std::to_string(idx) + "]"; std::string rv = cleanMmVal(mmKv[rk]); if (rv.empty()) break; if (rv.find("sim-pin (") != std::string::npos) { size_t p = rv.find('('); if (p != std::string::npos) d.mmPinRetries = std::atoi(rv.c_str() + p + 1); } else if (rv.find("sim-puk (") != std::string::npos) { size_t p = rv.find('('); if (p != std::string::npos) d.mmPukRetries = std::atoi(rv.c_str() + p + 1); } } } // 3. Query active SIM object if available if (!d.simDbusPath.empty()) { std::string simOut = runCaptureCmd( "mmcli -i " + shellQuote(d.simDbusPath) + " -K 2>/dev/null"); std::unordered_map simKv; parseKvLines(simOut, simKv, ':'); d.simIccid = cleanMmVal(simKv["sim.properties.iccid"]); d.simOperatorName = cleanMmVal(simKv["sim.properties.operator-name"]); if (d.operatorName.empty() && !d.simOperatorName.empty()) { d.operatorName = d.simOperatorName; } } // Sync ModemManager lock info into active slot if QMI didn't report retries int actIdx = (d.primaryGwSlot >= 1 && d.primaryGwSlot <= 2) ? (d.primaryGwSlot - 1) : (d.preferredSlot - 1); if (actIdx >= 0 && actIdx < 2) { if (d.mmUnlockRequired == "sim-pin" && d.slots[actIdx].pin1State == "not-initialized") { d.slots[actIdx].state = "present"; d.slots[actIdx].pin1State = "enabled-not-verified"; } else if (d.mmUnlockRequired == "sim-puk") { d.slots[actIdx].state = "present"; d.slots[actIdx].pin1State = "blocked"; } if (d.mmPinRetries >= 0 && d.slots[actIdx].pin1Retries == 0) { d.slots[actIdx].pin1Retries = d.mmPinRetries; } if (d.mmPukRetries >= 0 && d.slots[actIdx].puk1Retries == 0) { d.slots[actIdx].puk1Retries = d.mmPukRetries; } } return d; } std::vector loadRecentModemLog(const ContactBook& book, size_t maxEntries = 80) { std::vector out; std::ifstream in(getModemLogFilePath()); if (!in.is_open()) return out; std::vector lines; std::string line; while (std::getline(in, line)) { if (!line.empty()) lines.push_back(line); } for (auto it = lines.rbegin(); it != lines.rend() && out.size() < maxEntries; ++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); ModemLogRow row; row.timeStr = formatShortTime(trimStr(it->substr(0, t1))); row.eventType = trimStr(it->substr(t1 + 1, t2 - t1 - 1)); std::string rawNum = trimStr( t3 == std::string::npos ? it->substr(t2 + 1) : it->substr(t2 + 1, t3 - t2 - 1)); std::string cname = book.resolveName(rawNum); row.target = cname.empty() ? rawNum : (cname + " (" + rawNum + ")"); if (t3 != std::string::npos) { row.detail = trimStr(it->substr(t3 + 1)); } out.push_back(row); } return out; } int runModemApp(int argc, char** argv) { gRunning = 1; std::setlocale(LC_ALL, ""); XSetLocaleModifiers(""); bool isPreload = false; std::string openArg; for (int i = 1; i < argc; ++i) { if (!argv[i]) continue; if (std::strcmp(argv[i], "--preload") == 0) { isPreload = true; } else if (argv[i][0] != '-' && openArg.empty()) { openArg = argv[i]; } } std::string daemonPidPath = getPluginDaemonPidPath("modem"); std::string activePidPath = getRuntimeFile("xmodem.pid"); if (isPluginDaemonAlive("modem")) { if (!isPreload) { sendPluginCommand("modem", openArg.empty() ? "OPEN" : ("OPEN " + openArg)); } return 0; } writeIntFile(daemonPidPath, static_cast(::getpid())); int fifoFd = setupPluginDaemonFifo("modem"); 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); ThemeSpec initSpec; initSpec.loadActive(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[] = "xmodem"; char resClass[] = "XModem"; ch.res_name = resName; ch.res_class = resClass; XSetClassHint(dpy, win, &ch); XStoreName(dpy, win, "Modem & SIM"); 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& colOrange = pt.colOrange; auto& colRed = pt.colRed; ContactBook book; ModemStatusData status; std::vector logRows; TabId activeTab = TabId::SimCards; ModalMode modal = ModalMode::None; int simRowSel = 0; // 0 = SIM 1, 1 = SIM 2, 2 = Switch Active, 3 = Unlock PIN/PUK, 4 = PIN Lock Toggle, 5 = Change PIN int radioRowSel = 0; // 0 = Network Mode, 1 = USSD, 2 = Modem Power, 3 = Restart MM, 4 = Refresh int logRowSel = 0; int actionSel = 0; int targetSlot = 1; // PIN / PUK / Change PIN wizard state int wizardStep = 0; bool revealDigits = false; std::string inputPin1; std::string inputPin2; std::string inputPin3; std::string modalError; // USSD state std::string ussdInput = "*100#"; std::string ussdResponse; bool ussdWaitingReply = false; // AT Console state int atFocusRow = 0; // 0 = Input, 1 = Preset/Port, 2 = Output scroll int atPresetIdx = 0; int atPortIdx = 0; // 0 = auto, 1 = wwan0at0, 2 = wwan0at1 std::string atInput = "AT"; std::string atOutput = "Enter an AT command and press Send.\nUse Preset row (< >) for common commands."; int atScrollLine = 0; AtTextMode atTextMode = AtTextMode::Upper; int atLastTapKey = -1; int atLastTapIdx = 0; int64_t atLastTapMs = 0; std::string statusBanner; int64_t statusBannerUntilMs = 0; bool visible = false; int64_t openStartMs = 0; int64_t startupIgnoreUntilMs = 0; int64_t lastRefreshMs = 0; bool initialSoftkeysReleased = true; int softkeyPending = 0; int64_t softkeyDeadlineMs = 0; auto setBanner = [&](const std::string& msg, int durationMs = 2400) { statusBanner = msg; statusBannerUntilMs = nowMs() + durationMs; }; auto refreshData = [&](bool reloadLog = false) { status = queryModemAndSimStatus(); if (reloadLog || activeTab == TabId::ModemLog) { book.load(); logRows = loadRecentModemLog(book); if (logRowSel >= static_cast(logRows.size())) { logRowSel = std::max(0, static_cast(logRows.size()) - 1); } } lastRefreshMs = nowMs(); }; auto openPinOrPukModalForSlot = [&](int slotNum) { targetSlot = std::clamp(slotNum, 1, 2); const auto& s = status.slots[targetSlot - 1]; wizardStep = 0; revealDigits = false; inputPin1.clear(); inputPin2.clear(); inputPin3.clear(); modalError.clear(); if (s.needsPuk() || status.mmUnlockRequired == "sim-puk") { modal = ModalMode::UnblockPuk; } else { modal = ModalMode::VerifyPin; } }; auto applyOpenArg = [&](const std::string& arg) { std::string a = toLowerStr(trimStr(arg)); modal = ModalMode::None; if (a == "unlock" || a == "pin" || a == "puk") { activeTab = TabId::SimCards; int lockSlot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2) ? status.primaryGwSlot : status.preferredSlot; if (status.slots[0].needsPin() || status.slots[0].needsPuk()) { lockSlot = 1; } else if (status.slots[1].needsPin() || status.slots[1].needsPuk()) { lockSlot = 2; } simRowSel = lockSlot - 1; openPinOrPukModalForSlot(lockSlot); } else if (a == "info" || a == "status" || a == "radio") { activeTab = TabId::RadioInfo; } else if (a == "log" || a == "logs") { activeTab = TabId::ModemLog; book.load(); logRows = loadRecentModemLog(book); } else if (a == "at" || a == "atcmd") { activeTab = TabId::AtConsole; } else if (a == "ussd") { activeTab = TabId::RadioInfo; modal = ModalMode::UssdDialog; ussdResponse.clear(); ussdWaitingReply = false; } else { activeTab = TabId::SimCards; // If active SIM is locked, auto-highlight it if (status.slots[0].needsPin() || status.slots[0].needsPuk()) { simRowSel = 0; } else if (status.slots[1].needsPin() || status.slots[1].needsPuk()) { simRowSel = 1; } } }; auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, int h, const std::string& text, int maxW = -1) -> int { 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 pangoTextWidth = [&](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())); int tw = 0, th = 0; pango_layout_get_pixel_size(layout, &tw, &th); 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); int cw = pangoTextWidth(layoutMain, centerLabel); drawPango(layoutMain, &colWhite, (kWinWidth - cw) / 2, barY, 26, centerLabel); int rw = pangoTextWidth(layoutMain, rightLabel); drawPango(layoutMain, &colLight, kWinWidth - rw - 6, barY, 26, rightLabel); }; auto drawSignalBars = [&](int x, int y, int pct) { int bars = (pct <= 0) ? 0 : (pct < 25 ? 1 : (pct < 50 ? 2 : (pct < 75 ? 3 : 4))); for (int i = 0; i < 4; ++i) { int bh = 4 + i * 3; int bx = x + i * 5; int by = y + (14 - bh); XSetForeground(dpy, gc, (i < bars) ? pt.spec.success : pt.spec.border); XFillRectangle(dpy, pix, gc, bx, by, 3, bh); } }; auto maskPinStr = [&](const std::string& s) -> std::string { if (revealDigits) return s; return std::string(s.size(), '*'); }; auto render = [&]() { if (!visible) return; pt.checkAndReload(); XSetForeground(dpy, gc, pt.spec.bgMain); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); // Top Tab Bar (Y=0..30) XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 30); XSetForeground(dpy, gc, pt.spec.border); XDrawLine(dpy, pix, gc, 0, 29, kWinWidth, 29); const char* tabLabels[4] = {"1:SIMs", "2:Radio", "3:AT", "4:Log"}; int tabW = kWinWidth / 4; for (int t = 0; t < 4; ++t) { bool isSel = (static_cast(activeTab) == t); if (isSel) { XSetForeground(dpy, gc, pt.spec.selBg); XFillRectangle(dpy, pix, gc, t * tabW, 0, tabW, 29); XSetForeground(dpy, gc, pt.spec.accent); XFillRectangle(dpy, pix, gc, t * tabW, 27, tabW, 2); } int tw = pangoTextWidth(layoutSmall, tabLabels[t]); drawPango(layoutSmall, isSel ? &colWhite : &colMuted, t * tabW + (tabW - tw) / 2, 0, 28, tabLabels[t]); } const int contentY = 32; const int contentH = kWinHeight - 32 - 26; if (activeTab == TabId::SimCards) { // Draw SIM 1 & SIM 2 Cards (height 68px each) for (int i = 0; i < 2; ++i) { const auto& s = status.slots[i]; int cardY = contentY + i * 70; bool rowSelected = (simRowSel == i); XSetForeground(dpy, gc, rowSelected ? pt.spec.selBg : pt.spec.bgAltEven); XFillRectangle(dpy, pix, gc, 4, cardY, kWinWidth - 8, 66); XSetForeground(dpy, gc, rowSelected ? pt.spec.accent : pt.spec.border); XDrawRectangle(dpy, pix, gc, 4, cardY, kWinWidth - 9, 65); drawPango(layoutIcon, s.state == "present" ? &colGreen : &colMuted, 10, cardY + 2, 22, kIconSim); std::string title = "SIM Slot " + std::to_string(s.slotNum); if (s.isActiveProvisioned) { title += " [ACTIVE]"; } else if (s.isPreferred) { title += " [PREF]"; } drawPango(layoutMain, rowSelected ? &colWhite : &colLight, 30, cardY + 2, 22, title, 198); std::string line2; XftColor* line2Col = &colMuted; if (s.state == "present") { if (s.needsPuk()) { line2 = "BLOCKED (Enter PUK)"; line2Col = &colRed; } else if (s.needsPin()) { line2 = "LOCKED (Enter PIN)"; line2Col = &colOrange; } else if (s.isActiveProvisioned) { std::string op = status.operatorName.empty() ? "Ready" : status.operatorName; if (!status.ownNumber.empty()) op += " " + status.ownNumber; line2 = op; line2Col = &colGreen; } else { line2 = "Present (Standby " + s.appType + ")"; line2Col = &colBlue; } } else if (s.state == "error") { line2 = "No SIM (No ATR)"; } else { line2 = "Empty Slot"; } drawPango(layoutSmall, line2Col, 10, cardY + 24, 20, line2, 220); std::string line3; if (s.state == "present") { std::string pState = s.pin1State; if (pState == "enabled-not-verified") pState = "PIN Req"; else if (pState == "enabled-verified") pState = "PIN On"; else if (pState == "disabled") pState = "PIN Off"; else if (pState == "blocked") pState = "PUK Req"; line3 = pState + " (PIN:" + std::to_string(s.pin1Retries) + " PUK:" + std::to_string(s.puk1Retries) + ")"; } else { line3 = "Insert nano-SIM & restart/switch"; } drawPango(layoutSmall, rowSelected ? &colLight : &colMuted, 10, cardY + 44, 18, line3, 220); } // Quick Action Rows below the 2 SIM cards (4 rows, 24px each) int actSlot = (simRowSel == 1) ? 2 : 1; int otherSlot = (status.preferredSlot == 1) ? 2 : 1; std::string quickRows[4] = { std::string(kIconRefresh) + " Switch Active to SIM " + std::to_string(otherSlot), std::string(kIconUnlock) + " Unlock SIM (PIN / PUK)", std::string(kIconLock) + " Enable / Disable SIM PIN", std::string(kIconKey) + " Change SIM PIN Code", }; (void)actSlot; int baseY = contentY + 142; for (int r = 0; r < 4; ++r) { int ry = baseY + r * 23; bool isSel = (simRowSel == (r + 2)); XSetForeground(dpy, gc, isSel ? pt.spec.selBg : ((r % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd)); XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 22); drawPango(layoutSmall, isSel ? &colWhite : &colLight, 10, ry, 22, quickRows[r], kWinWidth - 20); } drawSoftkeyBar("Actions", "Select", "Close"); } else if (activeTab == TabId::RadioInfo) { // Modem summary card (height 102px) XSetForeground(dpy, gc, pt.spec.bgAltEven); XFillRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 8, 102); XSetForeground(dpy, gc, pt.spec.border); XDrawRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 9, 101); std::string stStr = status.mmPresent ? status.mmState : "nomodem"; if (!status.mmFailedReason.empty()) { stStr += " (" + status.mmFailedReason + ")"; } XftColor* stCol = &colRed; if (status.mmState == "connected" || status.mmState == "registered") { stCol = &colGreen; } else if (status.mmState == "searching" || status.mmState == "connecting") { stCol = &colOrange; } drawPango(layoutMain, stCol, 10, contentY + 2, 22, "State: " + stStr, 185); drawSignalBars(kWinWidth - 32, contentY + 6, status.signalQuality); std::string opLine = "Operator: " + (status.operatorName.empty() ? "--" : status.operatorName); if (!status.accessTech.empty()) { opLine += " [" + status.accessTech + "]"; } drawPango(layoutSmall, &colLight, 10, contentY + 24, 18, opLine, 220); std::string sigLine = "Signal: " + std::to_string(status.signalQuality) + "% | Slot: SIM " + (status.primaryGwSlot > 0 ? std::to_string(status.primaryGwSlot) : "--"); drawPango(layoutSmall, &colLight, 10, contentY + 42, 18, sigLine, 220); std::string imei1Line = "IMEI 1: " + (status.imei.empty() ? "--" : status.imei); drawPango(layoutSmall, &colMuted, 10, contentY + 60, 18, imei1Line, 220); std::string imei2Line = "IMEI 2: " + (status.imei2.empty() ? "--" : status.imei2); drawPango(layoutSmall, &colMuted, 10, contentY + 78, 18, imei2Line, 220); // 5 Interactive Radio Control Rows std::string modeLabel = kRadioModes[status.modePresetIdx].label; bool powerOn = (status.mmPowerState == "on" || status.mmState == "registered" || status.mmState == "connected" || status.mmState == "searching"); std::string radioRows[5] = { std::string(kIconSignal) + " Mode: < " + modeLabel + " >", std::string(kIconPhone) + " USSD Code (" + ussdInput + ")", std::string(kIconFlash) + " Radio Power: " + (powerOn ? "ON" : "OFF"), std::string(kIconRefresh) + " Restart ModemManager", std::string(kIconInfo) + " Refresh Status Now", }; int baseY = contentY + 106; for (int r = 0; r < 5; ++r) { int ry = baseY + r * 25; bool isSel = (radioRowSel == r); XSetForeground(dpy, gc, isSel ? pt.spec.selBg : ((r % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd)); XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 24); drawPango(layoutSmall, isSel ? &colWhite : &colLight, 10, ry, 24, radioRows[r], kWinWidth - 20); } drawSoftkeyBar("Refresh", "Apply", "Close"); } else if (activeTab == TabId::AtConsole) { const char* modeBadge = "[ABC]"; if (atTextMode == AtTextMode::Lower) modeBadge = "[abc]"; else if (atTextMode == AtTextMode::Num) modeBadge = "[123]"; else if (atTextMode == AtTextMode::Sym) modeBadge = "[Sym]"; const char* portNames[3] = {"auto", "wwan0at0", "wwan0at1"}; const char* portLabels[3] = {"Auto", "SIM1 (at0)", "SIM2 (at1)"}; (void)portNames; // Row 0: AT Command Input Box (Y=contentY..contentY+46) drawPango(layoutSmall, &colYellow, 8, contentY, 18, std::string(kIconTerm) + " AT Command (# mode, * +?=):"); int mbW = pangoTextWidth(layoutSmall, modeBadge); drawPango(layoutSmall, &colGreen, kWinWidth - mbW - 8, contentY, 18, modeBadge); bool focus0 = (atFocusRow == 0); XSetForeground(dpy, gc, focus0 ? pt.spec.bgInputFocus : pt.spec.bgAltEven); XFillRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 12, 26); XSetForeground(dpy, gc, focus0 ? pt.spec.accent : pt.spec.border); XDrawRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 13, 25); drawPango(layoutMain, &colWhite, 12, contentY + 19, 26, atInput + (focus0 ? "_" : ""), kWinWidth - 24); // Row 1: Preset & AT Port Selector (Y=contentY+49..contentY+73) bool focus1 = (atFocusRow == 1); XSetForeground(dpy, gc, focus1 ? pt.spec.selBg : pt.spec.bgAltOdd); XFillRectangle(dpy, pix, gc, 6, contentY + 48, kWinWidth - 12, 24); std::string presetRow = "< Preset: " + std::string(kAtPresets[atPresetIdx]) + " | *Port: " + portLabels[atPortIdx] + " >"; drawPango(layoutSmall, focus1 ? &colWhite : &colLight, 10, contentY + 48, 24, presetRow, kWinWidth - 20); // Row 2: AT Response Output Area (Y=contentY+75..contentY+contentH-2) bool focus2 = (atFocusRow == 2); int outY = contentY + 75; int outH = contentH - 77; XSetForeground(dpy, gc, pt.spec.bgAltEven); XFillRectangle(dpy, pix, gc, 6, outY, kWinWidth - 12, outH); XSetForeground(dpy, gc, focus2 ? pt.spec.accent : pt.spec.border); XDrawRectangle(dpy, pix, gc, 6, outY, kWinWidth - 13, outH - 1); pango_layout_set_width(layoutSmall, (kWinWidth - 24) * PANGO_SCALE); pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR); pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE); pango_layout_set_text(layoutSmall, atOutput.c_str(), static_cast(atOutput.size())); int lineCount = pango_layout_get_line_count(layoutSmall); int maxVisLines = std::max(1, (outH - 8) / 17); if (atScrollLine > std::max(0, lineCount - maxVisLines)) { atScrollLine = std::max(0, lineCount - maxVisLines); } int curY = outY + 4; for (int li = atScrollLine; li < lineCount && (li - atScrollLine) < maxVisLines; ++li) { PangoLayoutLine* pl = pango_layout_get_line_readonly(layoutSmall, li); if (pl) { PangoRectangle ink{}, logical{}; pango_layout_line_get_pixel_extents(pl, &ink, &logical); pango_xft_render_layout_line( xftDraw, &colLight, pl, 12 * PANGO_SCALE, (curY - logical.y) * PANGO_SCALE); curY += std::max(16, logical.height + 1); } } pango_layout_set_width(layoutSmall, -1); drawSoftkeyBar("Send", "Send", (atFocusRow == 0 && !atInput.empty()) ? "Clear" : "Close"); } else if (activeTab == TabId::ModemLog) { if (logRows.empty()) { drawPango(layoutMain, &colMuted, 16, contentY + 60, 24, "No modem events logged."); } else { const int rowH = 38; const int maxVis = contentH / rowH; // 6 rows int startIdx = std::clamp(logRowSel - maxVis / 2, 0, std::max(0, static_cast(logRows.size()) - maxVis)); for (int i = 0; i < maxVis && (startIdx + i) < static_cast(logRows.size()); ++i) { int idx = startIdx + i; const auto& row = logRows[idx]; int ry = contentY + i * rowH; bool isSel = (idx == logRowSel); XSetForeground(dpy, gc, isSel ? pt.spec.selBg : ((i % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd)); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); const char* ic = kIconInfo; XftColor* icCol = &colBlue; if (row.eventType.find("missed") != std::string::npos) { ic = kIconMissedCall; icCol = &colRed; } else if (row.eventType.find("call_") != std::string::npos) { ic = kIconPhone; icCol = &colGreen; } else if (row.eventType.find("txt") != std::string::npos) { ic = kIconSms; icCol = &colYellow; } drawPango(layoutIcon, icCol, 6, ry + 2, 18, ic); drawPango(layoutSmall, isSel ? &colWhite : &colLight, 26, ry + 2, 18, row.eventType + " | " + row.timeStr, 208); std::string sub = row.target; if (!row.detail.empty()) sub += " - " + row.detail; drawPango(layoutSmall, isSel ? &colLight : &colMuted, 26, ry + 19, 17, sub, 208); } } drawSoftkeyBar("Refresh", "Call/SMS", "Close"); } // Modal Overlays if (modal == ModalMode::SimActions) { XSetForeground(dpy, gc, pt.spec.bgOverlay); XFillRectangle(dpy, pix, gc, 12, 44, kWinWidth - 24, 196); XSetForeground(dpy, gc, pt.spec.accent); XDrawRectangle(dpy, pix, gc, 12, 44, kWinWidth - 25, 195); std::string hdr = "SIM Slot " + std::to_string(targetSlot) + " Actions"; drawPango(layoutMain, &colYellow, 20, 48, 24, hdr, kWinWidth - 40); const char* actLabels[5] = { "Set as Active SIM Slot", "Unlock SIM (Enter PIN)", "Unblock SIM (Enter PUK)", "Enable / Disable SIM PIN", "Change SIM PIN Code", }; for (int i = 0; i < 5; ++i) { int ry = 76 + i * 30; bool isSel = (actionSel == i); XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgButton); XFillRectangle(dpy, pix, gc, 18, ry, kWinWidth - 36, 26); drawPango(layoutSmall, isSel ? &colWhite : &colLight, 24, ry, 26, actLabels[i], kWinWidth - 48); } drawSoftkeyBar("Select", "OK", "Back"); } else if (modal == ModalMode::VerifyPin || modal == ModalMode::UnblockPuk || modal == ModalMode::TogglePinLock || modal == ModalMode::ChangePin) { XSetForeground(dpy, gc, pt.spec.bgOverlay); XFillRectangle(dpy, pix, gc, 10, 40, kWinWidth - 20, 210); XSetForeground(dpy, gc, pt.spec.accent); XDrawRectangle(dpy, pix, gc, 10, 40, kWinWidth - 21, 209); const auto& s = status.slots[targetSlot - 1]; std::string title; std::string prompt; std::string activeInput; std::string retriesText; if (modal == ModalMode::VerifyPin) { title = "Unlock SIM " + std::to_string(targetSlot) + " (PIN)"; prompt = "Enter 4-8 digit SIM PIN:"; activeInput = maskPinStr(inputPin1); retriesText = "PIN attempts remaining: " + std::to_string(s.pin1Retries); } else if (modal == ModalMode::UnblockPuk) { title = "Unblock SIM " + std::to_string(targetSlot) + " (PUK)"; retriesText = "PUK attempts remaining: " + std::to_string(s.puk1Retries); if (wizardStep == 0) { prompt = "Step 1/3: Enter 8-digit PUK:"; activeInput = revealDigits ? inputPin1 : inputPin1; // PUK is usually visible or toggleable } else if (wizardStep == 1) { prompt = "Step 2/3: Enter New PIN (4-8 digits):"; activeInput = maskPinStr(inputPin2); } else { prompt = "Step 3/3: Confirm New PIN:"; activeInput = maskPinStr(inputPin3); } } else if (modal == ModalMode::TogglePinLock) { bool enableTarget = !s.isPinEnabled(); title = (enableTarget ? "Enable" : "Disable") + std::string(" SIM ") + std::to_string(targetSlot) + " PIN"; prompt = "Enter current SIM PIN:"; activeInput = maskPinStr(inputPin1); retriesText = "PIN attempts remaining: " + std::to_string(s.pin1Retries); } else if (modal == ModalMode::ChangePin) { title = "Change SIM " + std::to_string(targetSlot) + " PIN"; retriesText = "PIN attempts remaining: " + std::to_string(s.pin1Retries); if (wizardStep == 0) { prompt = "Step 1/3: Current SIM PIN:"; activeInput = maskPinStr(inputPin1); } else if (wizardStep == 1) { prompt = "Step 2/3: New SIM PIN (4-8 digits):"; activeInput = maskPinStr(inputPin2); } else { prompt = "Step 3/3: Confirm New SIM PIN:"; activeInput = maskPinStr(inputPin3); } } drawPango(layoutMain, &colYellow, 18, 46, 24, title, kWinWidth - 36); drawPango(layoutSmall, (s.pin1Retries <= 1 && s.pin1Retries > 0) ? &colRed : &colOrange, 18, 72, 20, retriesText, kWinWidth - 36); drawPango(layoutSmall, &colLight, 18, 98, 20, prompt, kWinWidth - 36); // Input box XSetForeground(dpy, gc, pt.spec.bgInputFocus); XFillRectangle(dpy, pix, gc, 18, 122, kWinWidth - 36, 34); XSetForeground(dpy, gc, pt.spec.accent); XDrawRectangle(dpy, pix, gc, 18, 122, kWinWidth - 37, 33); std::string dispBox = activeInput + "_"; drawPango(layoutMain, &colWhite, 26, 122, 34, dispBox, kWinWidth - 52); drawPango(layoutSmall, &colMuted, 18, 164, 18, "Press [*] to Show/Hide digits", kWinWidth - 36); drawPango(layoutSmall, &colMuted, 18, 182, 18, "Press [BackSpace] to delete digit", kWinWidth - 36); if (!modalError.empty()) { drawPango(layoutSmall, &colRed, 18, 204, 20, modalError, kWinWidth - 36); } drawSoftkeyBar("Submit", "OK", "Cancel"); } else if (modal == ModalMode::UssdDialog) { XSetForeground(dpy, gc, pt.spec.bgOverlay); XFillRectangle(dpy, pix, gc, 10, 40, kWinWidth - 20, 210); XSetForeground(dpy, gc, pt.spec.accent); XDrawRectangle(dpy, pix, gc, 10, 40, kWinWidth - 21, 209); drawPango(layoutMain, &colYellow, 18, 46, 24, ussdWaitingReply ? "USSD Reply" : "Send USSD Code", kWinWidth - 36); XSetForeground(dpy, gc, pt.spec.bgInputFocus); XFillRectangle(dpy, pix, gc, 18, 74, kWinWidth - 36, 30); XSetForeground(dpy, gc, pt.spec.accent); XDrawRectangle(dpy, pix, gc, 18, 74, kWinWidth - 37, 29); drawPango(layoutMain, &colWhite, 24, 74, 30, ussdInput + "_", kWinWidth - 48); // Response area XSetForeground(dpy, gc, pt.spec.bgInput); XFillRectangle(dpy, pix, gc, 18, 110, kWinWidth - 36, 130); std::string respText = ussdResponse.empty() ? "Dial e.g. *100# and press Send." : ussdResponse; pango_layout_set_width(layoutSmall, (kWinWidth - 48) * PANGO_SCALE); pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR); pango_layout_set_text(layoutSmall, respText.c_str(), static_cast(respText.size())); pango_xft_render_layout(xftDraw, &colLight, layoutSmall, 24 * PANGO_SCALE, 116 * PANGO_SCALE); drawSoftkeyBar("Send", "Send", "Close"); } // Status Toast Banner if (!statusBanner.empty() && nowMs() < statusBannerUntilMs) { XSetForeground(dpy, gc, pt.spec.bgToast); XFillRectangle(dpy, pix, gc, 8, kWinHeight - 56, kWinWidth - 16, 26); XSetForeground(dpy, gc, pt.spec.accent); XDrawRectangle(dpy, pix, gc, 8, kWinHeight - 56, kWinWidth - 17, 25); drawPango(layoutSmall, &colWhite, 14, kWinHeight - 56, 26, statusBanner, kWinWidth - 28); } XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); XFlush(dpy); }; auto grabKeys = [&]() { for (int i = 0; i < 6; ++i) { if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime) == GrabSuccess) { break; } usleep(15000); } }; auto showWindow = [&](const std::string& arg = "") { closeOtherPlugins("modem"); refreshData(true); applyOpenArg(arg); writeIntFile(activePidPath, static_cast(::getpid())); if (!visible) { XMapRaised(dpy, win); visible = true; } else { XRaiseWindow(dpy, win); } openStartMs = nowMs(); startupIgnoreUntilMs = openStartMs + 180; initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); softkeyPending = 0; grabKeys(); render(); }; auto hideWindow = [&]() { if (!visible) return; int xfdLocal = ConnectionNumber(dpy); int64_t drainDeadline = nowMs() + 180; while (nowMs() < drainDeadline && querySoftkeysDown(dpy) != 0) { while (XPending(dpy) > 0) { XEvent ev; XNextEvent(dpy, &ev); } struct pollfd pfd {}; pfd.fd = xfdLocal; pfd.events = POLLIN; ::poll(&pfd, 1, 10); } writeSoftkeySuppress(nowMs() + 450); XUngrabKeyboard(dpy, CurrentTime); XUnmapWindow(dpy, win); XFlush(dpy); visible = false; modal = ModalMode::None; ::unlink(activePidPath.c_str()); }; auto switchSimSlotNow = [&](int slotNum) { slotNum = std::clamp(slotNum, 1, 2); setBanner("Switching active SIM to Slot " + std::to_string(slotNum) + "..."); render(); int rc = 0; runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection switch " + std::to_string(slotNum) + " >/dev/null 2>&1", &rc); runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1"); refreshData(false); if (rc == 0) { setBanner("Active SIM set to Slot " + std::to_string(slotNum)); } else { setBanner("Failed to switch SIM slot"); } }; auto submitModalInput = [&]() { if (modal == ModalMode::VerifyPin) { if (inputPin1.size() < 4 || inputPin1.size() > 8) { modalError = "PIN must be 4 to 8 digits"; return; } setBanner("Verifying SIM PIN..."); render(); int rc = -1; runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection verify-pin " + std::to_string(targetSlot) + " " + shellQuote(inputPin1) + " >/dev/null 2>&1", &rc); refreshData(false); runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1"); if (rc == 0) { modal = ModalMode::None; setBanner("SIM " + std::to_string(targetSlot) + " unlocked"); } else { inputPin1.clear(); modalError = "Incorrect PIN"; } } else if (modal == ModalMode::UnblockPuk) { if (wizardStep == 0) { if (inputPin1.size() != 8) { modalError = "PUK must be 8 digits"; return; } modalError.clear(); wizardStep = 1; } else if (wizardStep == 1) { if (inputPin2.size() < 4 || inputPin2.size() > 8) { modalError = "New PIN must be 4-8 digits"; return; } modalError.clear(); wizardStep = 2; } else { if (inputPin3 != inputPin2) { inputPin3.clear(); modalError = "PIN confirmation does not match"; return; } setBanner("Unblocking SIM with PUK..."); render(); int rc = -1; runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection unblock-puk " + std::to_string(targetSlot) + " " + shellQuote(inputPin1) + " " + shellQuote(inputPin2) + " >/dev/null 2>&1", &rc); refreshData(false); runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1"); if (rc == 0) { modal = ModalMode::None; setBanner("SIM unblocked and new PIN set"); } else { wizardStep = 0; inputPin1.clear(); inputPin2.clear(); inputPin3.clear(); modalError = "PUK unblock failed"; } } } else if (modal == ModalMode::TogglePinLock) { if (inputPin1.size() < 4 || inputPin1.size() > 8) { modalError = "PIN must be 4 to 8 digits"; return; } bool enableTarget = !status.slots[targetSlot - 1].isPinEnabled(); std::string modeStr = enableTarget ? "enable" : "disable"; setBanner((enableTarget ? "Enabling" : "Disabling") + std::string(" SIM PIN...")); render(); int rc = -1; runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection set-pin-lock " + std::to_string(targetSlot) + " " + modeStr + " " + shellQuote(inputPin1) + " >/dev/null 2>&1", &rc); refreshData(false); if (rc == 0) { modal = ModalMode::None; setBanner(std::string("SIM PIN protection ") + (enableTarget ? "enabled" : "disabled")); } else { inputPin1.clear(); modalError = "Failed (wrong PIN?)"; } } else if (modal == ModalMode::ChangePin) { if (wizardStep == 0) { if (inputPin1.size() < 4 || inputPin1.size() > 8) { modalError = "Current PIN must be 4-8 digits"; return; } modalError.clear(); wizardStep = 1; } else if (wizardStep == 1) { if (inputPin2.size() < 4 || inputPin2.size() > 8) { modalError = "New PIN must be 4-8 digits"; return; } modalError.clear(); wizardStep = 2; } else { if (inputPin3 != inputPin2) { inputPin3.clear(); modalError = "New PINs do not match"; return; } setBanner("Changing SIM PIN..."); render(); int rc = -1; runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection change-pin " + std::to_string(targetSlot) + " " + shellQuote(inputPin1) + " " + shellQuote(inputPin2) + " >/dev/null 2>&1", &rc); refreshData(false); if (rc == 0) { modal = ModalMode::None; setBanner("SIM PIN changed successfully"); } else { wizardStep = 0; inputPin1.clear(); inputPin2.clear(); inputPin3.clear(); modalError = "Failed to change PIN"; } } } else if (modal == ModalMode::UssdDialog) { if (ussdInput.empty()) return; if (ussdInput == "*#06#") { DeviceImeis im = queryDeviceImeis(); ussdResponse = "IMEI 1: " + (im.imei1.empty() ? "--" : im.imei1) + "\nIMEI 2: " + (im.imei2.empty() ? "--" : im.imei2) + "\nIMEI SV: " + (im.imeiSv.empty() ? "--" : im.imeiSv); ussdWaitingReply = false; return; } ussdResponse = "Sending USSD " + ussdInput + "..."; render(); ussdResponse = sendModemUssd(ussdInput, ussdWaitingReply, &ussdWaitingReply); if (ussdWaitingReply) { ussdInput.clear(); } } }; auto applyRadioModePreset = [&](int presetIdx) { presetIdx = (presetIdx + kNumRadioModes) % kNumRadioModes; status.modePresetIdx = presetIdx; const auto& p = kRadioModes[presetIdx]; setBanner(std::string("Setting mode: ") + p.label); render(); std::string cmd = "mmcli -m any --set-allowed-modes=" + shellQuote(p.allowed); if ( std::strcmp(p.preferred, "none") != 0) { cmd += " --set-preferred-mode=" + shellQuote(p.preferred); } cmd += " >/dev/null 2>&1"; int rc = 0; runCaptureCmd(cmd, &rc); refreshData(false); setBanner(rc == 0 ? (std::string("Mode: ") + p.label) : "Mode change failed"); }; auto triggerSimRowAction = [&](int row) { if (row == 0 || row == 1) { targetSlot = row + 1; actionSel = 0; modal = ModalMode::SimActions; } else if (row == 2) { int otherSlot = (status.preferredSlot == 1) ? 2 : 1; switchSimSlotNow(otherSlot); } else if (row == 3) { int slot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2) ? status.primaryGwSlot : status.preferredSlot; openPinOrPukModalForSlot(slot); } else if (row == 4) { targetSlot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2) ? status.primaryGwSlot : status.preferredSlot; wizardStep = 0; inputPin1.clear(); modalError.clear(); modal = ModalMode::TogglePinLock; } else if (row == 5) { targetSlot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2) ? status.primaryGwSlot : status.preferredSlot; wizardStep = 0; inputPin1.clear(); inputPin2.clear(); inputPin3.clear(); modalError.clear(); modal = ModalMode::ChangePin; } }; auto executeAtCommand = [&]() { std::string cmd = trimStr(atInput); if (cmd.empty()) { setBanner("AT command cannot be empty"); return; } const char* portArg = (atPortIdx == 1) ? "/dev/wwan0at0" : (atPortIdx == 2 ? "/dev/wwan0at1" : "auto"); atOutput = "Sending: " + cmd + " ..."; atScrollLine = 0; render(); std::string out = trimStr(runCaptureCmd( "doas -n /usr/libexec/msm-modem-uim-selection at " + shellQuote(portArg) + " " + shellQuote(cmd) + " 2>&1")); if (out.empty()) { atOutput = "> " + cmd + "\n(No response)"; } else { atOutput = "> " + cmd + "\n" + out; } atScrollLine = 0; setBanner("AT response received"); }; auto handleLeftSoftkey = [&]() { if (modal == ModalMode::SimActions) { int act = actionSel; modal = ModalMode::None; if (act == 0) { switchSimSlotNow(targetSlot); } else if (act == 1) { wizardStep = 0; inputPin1.clear(); modalError.clear(); modal = ModalMode::VerifyPin; } else if (act == 2) { wizardStep = 0; inputPin1.clear(); inputPin2.clear(); inputPin3.clear(); modalError.clear(); modal = ModalMode::UnblockPuk; } else if (act == 3) { wizardStep = 0; inputPin1.clear(); modalError.clear(); modal = ModalMode::TogglePinLock; } else if (act == 4) { wizardStep = 0; inputPin1.clear(); inputPin2.clear(); inputPin3.clear(); modalError.clear(); modal = ModalMode::ChangePin; } return; } if (modal != ModalMode::None) { submitModalInput(); return; } if (activeTab == TabId::SimCards) { targetSlot = (simRowSel == 1) ? 2 : 1; actionSel = 0; modal = ModalMode::SimActions; } else if (activeTab == TabId::RadioInfo) { refreshData(false); setBanner("Modem status refreshed"); } else if (activeTab == TabId::AtConsole) { executeAtCommand(); } else if (activeTab == TabId::ModemLog) { refreshData(true); setBanner("Modem log refreshed"); } }; auto handleRightSoftkey = [&]() { if (modal != ModalMode::None) { if (modal == ModalMode::UssdDialog) { runAsyncCmd("mmcli -m any --3gpp-ussd-cancel >/dev/null 2>&1"); } modal = ModalMode::None; return; } hideWindow(); }; auto handleEnterKey = [&]() { if (modal == ModalMode::SimActions) { handleLeftSoftkey(); return; } if (modal != ModalMode::None) { submitModalInput(); return; } if (activeTab == TabId::SimCards) { triggerSimRowAction(simRowSel); } else if (activeTab == TabId::RadioInfo) { if (radioRowSel == 0) { applyRadioModePreset(status.modePresetIdx + 1); } else if (radioRowSel == 1) { modal = ModalMode::UssdDialog; ussdResponse.clear(); ussdWaitingReply = false; } else if (radioRowSel == 2) { bool powerOn = (status.mmPowerState == "on" || status.mmState == "registered" || status.mmState == "connected" || status.mmState == "searching"); setBanner(powerOn ? "Disabling modem..." : "Enabling modem..."); render(); runCaptureCmd(powerOn ? "mmcli -m any -d >/dev/null 2>&1" : "mmcli -m any -e >/dev/null 2>&1"); refreshData(false); runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1"); } else if (radioRowSel == 3) { setBanner("Restarting ModemManager..."); render(); runCaptureCmd("doas -n rc-service modemmanager restart >/dev/null 2>&1"); refreshData(false); setBanner("ModemManager restarted"); } else if (radioRowSel == 4) { refreshData(false); setBanner("Refreshed"); } } else if (activeTab == TabId::AtConsole) { if (atFocusRow == 1) { atPortIdx = (atPortIdx + 1) % 3; } else { executeAtCommand(); } } else if (activeTab == TabId::ModemLog) { if (!logRows.empty() && logRowSel >= 0 && logRowSel < static_cast(logRows.size())) { const auto& r = logRows[logRowSel]; std::string num = normalizePhone(r.target); if (!num.empty()) { if (r.eventType.find("txt") != std::string::npos) { hideWindow(); if (!sendPluginCommand("sms", "OPEN thread:" + num)) { runAsyncArgv("xsms", "tailtextlog", num.c_str()); } } else { hideWindow(); if (!sendPluginCommand("dialer", "OPEN " + num)) { runAsyncArgv("xdialer", num.c_str()); } } } } } }; if (!isPreload) { showWindow(openArg); } int xfd = ConnectionNumber(dpy); while (gRunning) { while (XPending(dpy) > 0) { XEvent ev; XNextEvent(dpy, &ev); if (ev.type == Expose && ev.xexpose.count == 0) { render(); } else if (ev.type == VisibilityNotify && visible && ev.xvisibility.state != VisibilityUnobscured) { XRaiseWindow(dpy, win); } else if (ev.type == KeyRelease) { if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { initialSoftkeysReleased = true; } } else if (ev.type == KeyPress && visible) { int64_t now = nowMs(); if (now < startupIgnoreUntilMs) continue; KeySym sym = XLookupKeysym(&ev.xkey, 0); unsigned int kc = ev.xkey.keycode; bool isLSK = (kc == 135 || sym == XK_F1 || sym == XK_Menu || sym == XF86XK_TaskPane); bool isRSK = (kc == 166 || sym == XK_F2 || sym == XF86XK_Back || sym == XK_Escape || sym == static_cast(XF86XK_KbdLcdMenu2)); if (isLSK || isRSK) { if (!initialSoftkeysReleased) continue; int mask = querySoftkeysDown(dpy); if (mask == 3 || (softkeyPending != 0 && ((isLSK && softkeyPending == 2) || (isRSK && softkeyPending == 1)))) { softkeyPending = 0; writeSoftkeySuppress(now + 400); hideWindow(); sendPluginCommand("xdrawer", "TOGGLE"); continue; } softkeyPending = isLSK ? 1 : 2; softkeyDeadlineMs = now + 90; continue; } if (sym == XF86XK_HangupPhone || sym == XK_Cancel) { hideWindow(); continue; } // Modal numeric / code input handling if (modal == ModalMode::VerifyPin || modal == ModalMode::UnblockPuk || modal == ModalMode::TogglePinLock || modal == ModalMode::ChangePin) { char dch = mapDialKey(sym, kc, ev.xkey.state); if (dch == '*') { revealDigits = !revealDigits; render(); continue; } std::string* curStr = &inputPin1; size_t maxLen = (modal == ModalMode::UnblockPuk && wizardStep == 0) ? 8 : 8; if ((modal == ModalMode::UnblockPuk || modal == ModalMode::ChangePin) && wizardStep == 1) { curStr = &inputPin2; } else if ((modal == ModalMode::UnblockPuk || modal == ModalMode::ChangePin) && wizardStep == 2) { curStr = &inputPin3; } if (dch >= '0' && dch <= '9') { if (curStr->size() < maxLen) { curStr->push_back(dch); modalError.clear(); render(); } continue; } if (sym == XK_BackSpace || sym == XK_Delete) { if (!curStr->empty()) { curStr->pop_back(); modalError.clear(); render(); } else if (wizardStep > 0) { --wizardStep; render(); } continue; } if (sym == XK_Return || sym == XK_KP_Enter) { submitModalInput(); render(); continue; } continue; } if (modal == ModalMode::UssdDialog) { char dch = mapDialKey(sym, kc, ev.xkey.state); if (dch != '\0') { if (ussdInput.size() < 32) { ussdInput.push_back(dch); render(); } continue; } if (sym == XK_BackSpace || sym == XK_Delete) { if (!ussdInput.empty()) { ussdInput.pop_back(); render(); } continue; } if (sym == XK_Return || sym == XK_KP_Enter || sym == XF86XK_Send || sym == XF86XK_Phone || sym == XF86XK_PickupPhone) { submitModalInput(); render(); continue; } continue; } if (modal == ModalMode::SimActions) { if (sym == XK_Up || sym == XK_KP_Up) { actionSel = (actionSel + 4) % 5; render(); } else if (sym == XK_Down || sym == XK_KP_Down) { actionSel = (actionSel + 1) % 5; render(); } else if (sym == XK_Return || sym == XK_KP_Enter) { handleEnterKey(); render(); } continue; } // AT Console text input when atFocusRow == 0 char dch = mapDialKey(sym, kc, ev.xkey.state); if (activeTab == TabId::AtConsole && atFocusRow == 0) { if (sym == XK_BackSpace || sym == XK_Delete) { if (!atInput.empty()) { atInput.pop_back(); atLastTapKey = -1; render(); } continue; } if (dch == '#') { atTextMode = static_cast( (static_cast(atTextMode) + 1) % 4); atLastTapKey = -1; render(); continue; } if (dch == '*') { const char* syms = "+?=,1\"':;/-_*#"; int len = static_cast(std::strlen(syms)); if (atLastTapKey == 10 && (now - atLastTapMs) < 850 && !atInput.empty()) { atLastTapIdx = (atLastTapIdx + 1) % len; atInput.back() = syms[atLastTapIdx]; } else if (atInput.size() < 64) { atLastTapKey = 10; atLastTapIdx = 0; atInput.push_back(syms[0]); } atLastTapMs = now; render(); continue; } if (dch >= '0' && dch <= '9') { int digit = dch - '0'; if (atTextMode == AtTextMode::Num) { if (atInput.size() < 64) { atInput.push_back(dch); atLastTapKey = -1; } } else if (atTextMode == AtTextMode::Sym) { const char* symTable = "+?=,.\"':;/-_"; char sc = symTable[digit % 10]; if (atInput.size() < 64) { atInput.push_back(sc); atLastTapKey = -1; } } else { const char* seq = kAtTapMap[digit]; int len = static_cast(std::strlen(seq)); if (atLastTapKey == digit && (now - atLastTapMs) < 850 && !atInput.empty()) { atLastTapIdx = (atLastTapIdx + 1) % len; char chOut = seq[atLastTapIdx]; if (atTextMode == AtTextMode::Lower) { chOut = static_cast( std::tolower(static_cast(chOut))); } atInput.back() = chOut; } else if (atInput.size() < 64) { atLastTapKey = digit; atLastTapIdx = 0; char chOut = seq[0]; if (atTextMode == AtTextMode::Lower) { chOut = static_cast( std::tolower(static_cast(chOut))); } atInput.push_back(chOut); } atLastTapMs = now; } render(); continue; } } // Main View Navigation (4 tabs: 1:SIMs, 2:Radio, 3:AT, 4:Log) if (dch == '1') { activeTab = TabId::SimCards; render(); continue; } else if (dch == '2') { activeTab = TabId::RadioInfo; render(); continue; } else if (dch == '3') { activeTab = TabId::AtConsole; render(); continue; } else if (dch == '4') { activeTab = TabId::ModemLog; refreshData(true); render(); continue; } else if (activeTab == TabId::AtConsole && dch == '*') { atPortIdx = (atPortIdx + 1) % 3; render(); continue; } if (sym == XK_Left || sym == XK_KP_Left) { if (activeTab == TabId::RadioInfo && radioRowSel == 0) { applyRadioModePreset(status.modePresetIdx - 1); } else if (activeTab == TabId::AtConsole && atFocusRow == 1) { atPresetIdx = (atPresetIdx + kNumAtPresets - 1) % kNumAtPresets; atInput = kAtPresets[atPresetIdx]; } else { activeTab = static_cast((static_cast(activeTab) + 3) % 4); if (activeTab == TabId::ModemLog) refreshData(true); } render(); } else if (sym == XK_Right || sym == XK_KP_Right) { if (activeTab == TabId::RadioInfo && radioRowSel == 0) { applyRadioModePreset(status.modePresetIdx + 1); } else if (activeTab == TabId::AtConsole && atFocusRow == 1) { atPresetIdx = (atPresetIdx + 1) % kNumAtPresets; atInput = kAtPresets[atPresetIdx]; } else { activeTab = static_cast((static_cast(activeTab) + 1) % 4); if (activeTab == TabId::ModemLog) refreshData(true); } render(); } else if (sym == XK_Up || sym == XK_KP_Up) { if (activeTab == TabId::SimCards) { simRowSel = (simRowSel + 5) % 6; } else if (activeTab == TabId::RadioInfo) { radioRowSel = (radioRowSel + 4) % 5; } else if (activeTab == TabId::AtConsole) { if (atFocusRow == 2 && atScrollLine > 0) { --atScrollLine; } else { atFocusRow = (atFocusRow + 2) % 3; } } else if (activeTab == TabId::ModemLog && !logRows.empty()) { logRowSel = std::max(0, logRowSel - 1); } render(); } else if (sym == XK_Down || sym == XK_KP_Down) { if (activeTab == TabId::SimCards) { simRowSel = (simRowSel + 1) % 6; } else if (activeTab == TabId::RadioInfo) { radioRowSel = (radioRowSel + 1) % 5; } else if (activeTab == TabId::AtConsole) { if (atFocusRow == 2) { ++atScrollLine; } else { atFocusRow = (atFocusRow + 1) % 3; } } else if (activeTab == TabId::ModemLog && !logRows.empty()) { logRowSel = std::min(static_cast(logRows.size()) - 1, logRowSel + 1); } render(); } else if (sym == XK_Return || sym == XK_KP_Enter || sym == XF86XK_Send || sym == XF86XK_Phone || sym == XF86XK_PickupPhone) { handleEnterKey(); render(); } } } int timeoutMs = -1; int64_t now = nowMs(); if (visible) { if (softkeyPending != 0) { int64_t rem = softkeyDeadlineMs - now; if (rem <= 0) { int pend = softkeyPending; softkeyPending = 0; if (querySoftkeysDown(dpy) == 3) { writeSoftkeySuppress(now + 400); hideWindow(); sendPluginCommand("xdrawer", "TOGGLE"); } else if (pend == 1) { handleLeftSoftkey(); render(); } else if (pend == 2) { handleRightSoftkey(); render(); } } else { timeoutMs = static_cast(rem); } } if (!statusBanner.empty()) { int64_t bRem = statusBannerUntilMs - now; if (bRem <= 0) { statusBanner.clear(); render(); } else if (timeoutMs < 0 || bRem < timeoutMs) { timeoutMs = static_cast(bRem); } } // Periodic 5-second status refresh while visible on SIM/Radio tabs if (modal == ModalMode::None && activeTab != TabId::ModemLog && activeTab != TabId::AtConsole) { int64_t rRem = (lastRefreshMs + 5000) - now; if (rRem <= 0) { refreshData(false); render(); rRem = 5000; } if (timeoutMs < 0 || rRem < timeoutMs) { timeoutMs = static_cast(rRem); } } } struct pollfd pfds[2]; pfds[0].fd = xfd; pfds[0].events = POLLIN; pfds[1].fd = fifoFd; pfds[1].events = POLLIN; int pr = ::poll(pfds, (fifoFd >= 0) ? 2 : 1, timeoutMs); if (pr > 0 && fifoFd >= 0 && (pfds[1].revents & POLLIN)) { for (const auto& cmd : readFifoLines(fifoFd)) { if (cmd == "CLOSE") { hideWindow(); } else if (cmd == "RELOAD_THEME") { pt.reload(); if (visible) render(); } else if (cmd == "TOGGLE") { if (visible) hideWindow(); else showWindow(); } else if (cmd.rfind("OPEN", 0) == 0) { std::string arg = (cmd.size() > 5) ? trimStr(cmd.substr(5)) : ""; showWindow(arg); } } } } hideWindow(); if (fifoFd >= 0) ::close(fifoFd); ::unlink(daemonPidPath.c_str()); XftDrawDestroy(xftDraw); XFreePixmap(dpy, pix); XFreeGC(dpy, gc); XDestroyWindow(dpy, win); XCloseDisplay(dpy); return 0; } int pluginInit(const DwmHostApi* host) { if (host && host->spawn_plugin) { host->spawn_plugin("modem", "--preload"); } return 0; } void pluginCleanup(void) {} } // namespace extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { DWM_PLUGIN_API_VERSION, "modem", "Bananarchy Dual-SIM, PIN/PUK & Modem Manager (240x320)", pluginInit, pluginCleanup, nullptr, nullptr, nullptr, runModemApp };