1904 lines
67 KiB
C++
1904 lines
67 KiB
C++
// 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 <X11/Xatom.h>
|
|
#include <X11/Xft/Xft.h>
|
|
#include <X11/Xutil.h>
|
|
#include <pango/pangoxft.h>
|
|
#include <poll.h>
|
|
#include <sys/stat.h>
|
|
#include <clocale>
|
|
|
|
#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<int>(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<int>(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<std::string, std::string>& 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<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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<ModemLogRow> loadRecentModemLog(const ContactBook& book,
|
|
size_t maxEntries = 80) {
|
|
std::vector<ModemLogRow> out;
|
|
std::ifstream in(getModemLogFilePath());
|
|
if (!in.is_open()) return out;
|
|
|
|
std::vector<std::string> 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<int>(::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<ModemLogRow> 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<int>(logRows.size())) {
|
|
logRowSel = std::max(0, static_cast<int>(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<int>(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<int>(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<int>(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<int>(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<int>(logRows.size()) - maxVis));
|
|
for (int i = 0; i < maxVis && (startIdx + i) < static_cast<int>(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<int>(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<int>(::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<int>(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<KeySym>(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<AtTextMode>(
|
|
(static_cast<int>(atTextMode) + 1) % 4);
|
|
atLastTapKey = -1;
|
|
render();
|
|
continue;
|
|
}
|
|
if (dch == '*') {
|
|
const char* syms = "+?=,1\"':;/-_*#";
|
|
int len = static_cast<int>(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<int>(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<char>(
|
|
std::tolower(static_cast<unsigned char>(chOut)));
|
|
}
|
|
atInput.back() = chOut;
|
|
} else if (atInput.size() < 64) {
|
|
atLastTapKey = digit;
|
|
atLastTapIdx = 0;
|
|
char chOut = seq[0];
|
|
if (atTextMode == AtTextMode::Lower) {
|
|
chOut = static_cast<char>(
|
|
std::tolower(static_cast<unsigned char>(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<TabId>((static_cast<int>(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<TabId>((static_cast<int>(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<int>(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<int>(rem);
|
|
}
|
|
}
|
|
if (!statusBanner.empty()) {
|
|
int64_t bRem = statusBannerUntilMs - now;
|
|
if (bRem <= 0) {
|
|
statusBanner.clear();
|
|
render();
|
|
} else if (timeoutMs < 0 || bRem < timeoutMs) {
|
|
timeoutMs = static_cast<int>(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<int>(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
|
|
};
|