Add ringtones/wallpapers plugins, AT console, dialer USSD/*#06#, and SIM icon fix
This commit is contained in:
@@ -152,6 +152,19 @@ int dialer_run_main(int argc, char** argv) {
|
||||
bool zeroHeld = false;
|
||||
int64_t zeroPressMs = 0;
|
||||
|
||||
enum class DialerModal {
|
||||
Inactive = 0,
|
||||
Imei = 1,
|
||||
Ussd = 2,
|
||||
};
|
||||
DialerModal modal = DialerModal::Inactive;
|
||||
DeviceImeis deviceImeis;
|
||||
std::string ussdCode;
|
||||
std::string ussdResponse;
|
||||
std::string ussdReplyInput;
|
||||
bool ussdSessionActive = false;
|
||||
int ussdScrollLine = 0;
|
||||
|
||||
auto updateFilter = [&]() {
|
||||
filtered.clear();
|
||||
if (number.empty()) {
|
||||
@@ -256,15 +269,23 @@ int dialer_run_main(int argc, char** argv) {
|
||||
subText = "Dial: " + c.number;
|
||||
subCol = &colYellow;
|
||||
} else if (!number.empty()) {
|
||||
std::string exactName = book.resolveName(number);
|
||||
if (!exactName.empty()) {
|
||||
subText = exactName;
|
||||
subCol = &colGreen;
|
||||
} else if (!filtered.empty()) {
|
||||
subText = filtered[0].name + " (" + filtered[0].number + ")";
|
||||
subCol = &colBlue;
|
||||
if (number == "*#06#") {
|
||||
subText = "Show Device IMEI (1 & 2)";
|
||||
subCol = &colYellow;
|
||||
} else if (isUssdCode(number)) {
|
||||
subText = "USSD Code (Press Call/OK to send)";
|
||||
subCol = &colYellow;
|
||||
} else {
|
||||
subText = "Press Call to dial (Hold 0: +)";
|
||||
std::string exactName = book.resolveName(number);
|
||||
if (!exactName.empty()) {
|
||||
subText = exactName;
|
||||
subCol = &colGreen;
|
||||
} else if (!filtered.empty()) {
|
||||
subText = filtered[0].name + " (" + filtered[0].number + ")";
|
||||
subCol = &colBlue;
|
||||
} else {
|
||||
subText = "Press Call to dial (Hold 0: +)";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subText = "0-9/*/# Dial | Hold 0: +";
|
||||
@@ -339,9 +360,111 @@ int dialer_run_main(int argc, char** argv) {
|
||||
XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, 1);
|
||||
|
||||
std::string leftLabel = number.empty() ? "Call Log" : "+Sign";
|
||||
std::string centerLabel = "CALL";
|
||||
std::string centerLabel =
|
||||
(selected < 0 && isUssdCode(number)) ? "SEND" : "CALL";
|
||||
std::string rightLabel = number.empty() ? "Close" : "Clear";
|
||||
|
||||
if (modal == DialerModal::Imei) {
|
||||
XSetForeground(dpy, gc, theme.spec.bgOverlay);
|
||||
XFillRectangle(dpy, pix, gc, 10, 22, kWinWidth - 20, kWinHeight - kSoftbarH - 34);
|
||||
XSetForeground(dpy, gc, theme.spec.accent);
|
||||
XDrawRectangle(dpy, pix, gc, 10, 22, kWinWidth - 21, kWinHeight - kSoftbarH - 35);
|
||||
|
||||
drawPango(layoutIcon, &colYellow, 18, 28, 24, kIconInfo);
|
||||
drawPango(layoutMain, &colYellow, 38, 28, 24, "Device IMEI (*#06#)", 184);
|
||||
|
||||
XSetForeground(dpy, gc, theme.spec.divider);
|
||||
XFillRectangle(dpy, pix, gc, 16, 54, kWinWidth - 32, 1);
|
||||
|
||||
// Slot 1 IMEI card
|
||||
XSetForeground(dpy, gc, theme.spec.bgAltEven);
|
||||
XFillRectangle(dpy, pix, gc, 16, 62, kWinWidth - 32, 54);
|
||||
XSetForeground(dpy, gc, theme.spec.border);
|
||||
XDrawRectangle(dpy, pix, gc, 16, 62, kWinWidth - 33, 53);
|
||||
drawPango(layoutSmall, &colBlue, 22, 65, 18, "IMEI 1 (SIM Slot 1):");
|
||||
drawPango(layoutMain, &colWhite, 22, 85, 24,
|
||||
deviceImeis.imei1.empty() ? "Unavailable" : deviceImeis.imei1,
|
||||
kWinWidth - 44);
|
||||
|
||||
// Slot 2 IMEI card
|
||||
XSetForeground(dpy, gc, theme.spec.bgAltEven);
|
||||
XFillRectangle(dpy, pix, gc, 16, 124, kWinWidth - 32, 54);
|
||||
XSetForeground(dpy, gc, theme.spec.border);
|
||||
XDrawRectangle(dpy, pix, gc, 16, 124, kWinWidth - 33, 53);
|
||||
drawPango(layoutSmall, &colGreen, 22, 127, 18, "IMEI 2 (SIM Slot 2):");
|
||||
drawPango(layoutMain, &colWhite, 22, 147, 24,
|
||||
deviceImeis.imei2.empty() ? "Unavailable" : deviceImeis.imei2,
|
||||
kWinWidth - 44);
|
||||
|
||||
std::string svLine = "IMEI SV: " +
|
||||
(deviceImeis.imeiSv.empty() ? "--" : deviceImeis.imeiSv);
|
||||
drawPango(layoutSmall, &colMuted, 22, 188, 20, svLine, kWinWidth - 44);
|
||||
|
||||
leftLabel = "";
|
||||
centerLabel = "OK";
|
||||
rightLabel = "Close";
|
||||
} else if (modal == DialerModal::Ussd) {
|
||||
XSetForeground(dpy, gc, theme.spec.bgOverlay);
|
||||
XFillRectangle(dpy, pix, gc, 8, 14, kWinWidth - 16, kWinHeight - kSoftbarH - 22);
|
||||
XSetForeground(dpy, gc, theme.spec.accent);
|
||||
XDrawRectangle(dpy, pix, gc, 8, 14, kWinWidth - 17, kWinHeight - kSoftbarH - 23);
|
||||
|
||||
std::string hdr = "USSD (" + ussdCode + ")";
|
||||
drawPango(layoutMain, &colYellow, 16, 18, 22, hdr, kWinWidth - 32);
|
||||
|
||||
// Response area
|
||||
XSetForeground(dpy, gc, theme.spec.bgAltEven);
|
||||
XFillRectangle(dpy, pix, gc, 14, 42, kWinWidth - 28, 152);
|
||||
XSetForeground(dpy, gc, theme.spec.border);
|
||||
XDrawRectangle(dpy, pix, gc, 14, 42, kWinWidth - 29, 151);
|
||||
|
||||
pango_layout_set_width(layoutSmall, (kWinWidth - 40) * PANGO_SCALE);
|
||||
pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR);
|
||||
pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE);
|
||||
pango_layout_set_text(layoutSmall, ussdResponse.c_str(),
|
||||
static_cast<int>(ussdResponse.size()));
|
||||
int lineCount = pango_layout_get_line_count(layoutSmall);
|
||||
int maxVisLines = 7;
|
||||
if (ussdScrollLine > std::max(0, lineCount - maxVisLines)) {
|
||||
ussdScrollLine = std::max(0, lineCount - maxVisLines);
|
||||
}
|
||||
int curY = 46;
|
||||
for (int li = ussdScrollLine;
|
||||
li < lineCount && (li - ussdScrollLine) < 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, &colWhite, pl, 20 * PANGO_SCALE,
|
||||
(curY - logical.y) * PANGO_SCALE);
|
||||
curY += std::max(18, logical.height + 2);
|
||||
}
|
||||
}
|
||||
pango_layout_set_width(layoutSmall, -1);
|
||||
|
||||
// Reply / Follow-up USSD input box
|
||||
drawPango(layoutSmall, ussdSessionActive ? &colGreen : &colMuted,
|
||||
16, 198, 18,
|
||||
ussdSessionActive ? "Session active - Reply:" : "Reply or dial USSD:");
|
||||
XSetForeground(dpy, gc, theme.spec.bgInputFocus);
|
||||
XFillRectangle(dpy, pix, gc, 14, 216, kWinWidth - 28, 26);
|
||||
XSetForeground(dpy, gc, theme.spec.selBg);
|
||||
XDrawRectangle(dpy, pix, gc, 14, 216, kWinWidth - 29, 25);
|
||||
drawPango(layoutMain, &colWhite, 20, 216, 26, ussdReplyInput + "_",
|
||||
kWinWidth - 40);
|
||||
|
||||
if (!ussdReplyInput.empty() || ussdSessionActive) {
|
||||
leftLabel = "Send";
|
||||
centerLabel = "SEND";
|
||||
rightLabel = "Close";
|
||||
} else {
|
||||
leftLabel = "Retry";
|
||||
centerLabel = "OK";
|
||||
rightLabel = "Close";
|
||||
}
|
||||
}
|
||||
|
||||
drawPango(layoutMain, &colLight, 6, sbY + 1, kSoftbarH - 1, leftLabel);
|
||||
int cw = pangoWidth(layoutMain, centerLabel);
|
||||
drawPango(layoutMain, &colGreen, (kWinWidth - cw) / 2, sbY + 1,
|
||||
@@ -367,8 +490,31 @@ int dialer_run_main(int argc, char** argv) {
|
||||
render();
|
||||
XSync(dpy, False);
|
||||
|
||||
auto openImeiModal = [&]() {
|
||||
deviceImeis = queryDeviceImeis();
|
||||
modal = DialerModal::Imei;
|
||||
number.clear();
|
||||
selected = allContacts.empty() ? -1 : 0;
|
||||
updateFilter();
|
||||
render();
|
||||
};
|
||||
|
||||
auto executeUssd = [&](const std::string& codeOrReply, bool isReply) {
|
||||
modal = DialerModal::Ussd;
|
||||
if (!isReply || isUssdCode(codeOrReply)) {
|
||||
ussdCode = codeOrReply;
|
||||
}
|
||||
ussdScrollLine = 0;
|
||||
ussdResponse = "Sending USSD " + codeOrReply + "...";
|
||||
ussdReplyInput.clear();
|
||||
render();
|
||||
ussdResponse = sendModemUssd(codeOrReply, isReply, &ussdSessionActive);
|
||||
render();
|
||||
};
|
||||
|
||||
auto showWindow = [&](const std::string& arg) {
|
||||
closeOtherPlugins("dialer");
|
||||
modal = DialerModal::Inactive;
|
||||
bool themeChanged = theme.checkAndReload(
|
||||
dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon,
|
||||
layoutNum, win);
|
||||
@@ -394,7 +540,9 @@ int dialer_run_main(int argc, char** argv) {
|
||||
usleep(2000);
|
||||
}
|
||||
bool reloaded = reloadContactsIfNeeded();
|
||||
if (!cleanArg.empty() || reloaded || themeChanged || number != cleanArg) {
|
||||
if (cleanArg == "*#06#") {
|
||||
openImeiModal();
|
||||
} else if (!cleanArg.empty() || reloaded || themeChanged || number != cleanArg) {
|
||||
number = cleanArg;
|
||||
selected = number.empty() ? (allContacts.empty() ? -1 : 0) : -1;
|
||||
updateFilter();
|
||||
@@ -408,8 +556,12 @@ int dialer_run_main(int argc, char** argv) {
|
||||
}
|
||||
number += cleanArg;
|
||||
selected = -1;
|
||||
updateFilter();
|
||||
render();
|
||||
if (number == "*#06#") {
|
||||
openImeiModal();
|
||||
} else {
|
||||
updateFilter();
|
||||
render();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -440,7 +592,13 @@ int dialer_run_main(int argc, char** argv) {
|
||||
softkeyPending = 0;
|
||||
softkeyDeadlineMs = 0;
|
||||
zeroHeld = false;
|
||||
if (!number.empty() || selected != (allContacts.empty() ? -1 : 0)) {
|
||||
if (modal == DialerModal::Ussd && ussdSessionActive) {
|
||||
cancelModemUssd();
|
||||
ussdSessionActive = false;
|
||||
}
|
||||
if (modal != DialerModal::Inactive || !number.empty() ||
|
||||
selected != (allContacts.empty() ? -1 : 0)) {
|
||||
modal = DialerModal::Inactive;
|
||||
number.clear();
|
||||
selected = allContacts.empty() ? -1 : 0;
|
||||
updateFilter();
|
||||
@@ -557,6 +715,79 @@ int dialer_run_main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state);
|
||||
|
||||
if (modal == DialerModal::Imei) {
|
||||
if (sym == XK_Return || sym == XK_KP_Enter || sym == XK_Escape ||
|
||||
sym == XK_BackSpace || sym == XF86XK_Phone ||
|
||||
sym == XF86XK_PickupPhone) {
|
||||
modal = DialerModal::Inactive;
|
||||
render();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (modal == DialerModal::Ussd) {
|
||||
if (dialCh != '\0') {
|
||||
if (ussdReplyInput.size() < 32) {
|
||||
ussdReplyInput.push_back(dialCh);
|
||||
render();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (sym == XK_BackSpace || sym == XK_Left || sym == XK_KP_Left) {
|
||||
if (!ussdReplyInput.empty()) {
|
||||
ussdReplyInput.pop_back();
|
||||
render();
|
||||
} else if (sym == XK_BackSpace) {
|
||||
if (ussdSessionActive) {
|
||||
cancelModemUssd();
|
||||
ussdSessionActive = false;
|
||||
}
|
||||
modal = DialerModal::Inactive;
|
||||
render();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (sym == XK_Escape) {
|
||||
if (ussdSessionActive) {
|
||||
cancelModemUssd();
|
||||
ussdSessionActive = false;
|
||||
}
|
||||
modal = DialerModal::Inactive;
|
||||
render();
|
||||
continue;
|
||||
}
|
||||
if (sym == XK_Up || sym == XK_KP_Up) {
|
||||
if (ussdScrollLine > 0) {
|
||||
--ussdScrollLine;
|
||||
render();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (sym == XK_Down || sym == XK_KP_Down) {
|
||||
++ussdScrollLine;
|
||||
render();
|
||||
continue;
|
||||
}
|
||||
if (sym == XK_Return || sym == XK_KP_Enter ||
|
||||
sym == XF86XK_Phone || sym == XF86XK_PickupPhone ||
|
||||
sym == XF86XK_Send) {
|
||||
if (!ussdReplyInput.empty()) {
|
||||
if (ussdReplyInput == "*#06#") {
|
||||
openImeiModal();
|
||||
} else {
|
||||
std::string req = ussdReplyInput;
|
||||
executeUssd(req, !isUssdCode(req));
|
||||
}
|
||||
} else if (!ussdSessionActive) {
|
||||
modal = DialerModal::Inactive;
|
||||
render();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dialCh != '\0') {
|
||||
if (nowMs() - openStartMs < 40 && number.size() == 1 &&
|
||||
number[0] == dialCh) {
|
||||
@@ -570,6 +801,10 @@ int dialer_run_main(int argc, char** argv) {
|
||||
} else {
|
||||
zeroHeld = false;
|
||||
}
|
||||
if (number == "*#06#") {
|
||||
openImeiModal();
|
||||
continue;
|
||||
}
|
||||
updateFilter();
|
||||
render();
|
||||
continue;
|
||||
@@ -632,12 +867,22 @@ int dialer_run_main(int argc, char** argv) {
|
||||
case XK_Return:
|
||||
case XK_KP_Enter:
|
||||
case XF86XK_Phone:
|
||||
case XF86XK_PickupPhone: {
|
||||
case XF86XK_PickupPhone:
|
||||
case XF86XK_Send: {
|
||||
std::string target = selectActiveNumber();
|
||||
if (!target.empty()) {
|
||||
writeSoftkeySuppress(nowMs() + 350);
|
||||
hideWindow(true);
|
||||
placePhoneCall(target);
|
||||
if (target == "*#06#") {
|
||||
openImeiModal();
|
||||
} else if (isUssdCode(target)) {
|
||||
number.clear();
|
||||
selected = allContacts.empty() ? -1 : 0;
|
||||
updateFilter();
|
||||
executeUssd(target, false);
|
||||
} else {
|
||||
writeSoftkeySuppress(nowMs() + 350);
|
||||
hideWindow(true);
|
||||
placePhoneCall(target);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -658,7 +903,21 @@ int dialer_run_main(int argc, char** argv) {
|
||||
runAsyncArgv("xdrawer");
|
||||
}
|
||||
} else if (comb == 1) {
|
||||
if (number.empty()) {
|
||||
if (modal == DialerModal::Imei) {
|
||||
modal = DialerModal::Inactive;
|
||||
render();
|
||||
} else if (modal == DialerModal::Ussd) {
|
||||
if (!ussdReplyInput.empty()) {
|
||||
if (ussdReplyInput == "*#06#") {
|
||||
openImeiModal();
|
||||
} else {
|
||||
std::string req = ussdReplyInput;
|
||||
executeUssd(req, !isUssdCode(req));
|
||||
}
|
||||
} else if (!ussdCode.empty()) {
|
||||
executeUssd(ussdCode, false);
|
||||
}
|
||||
} else if (number.empty()) {
|
||||
writeSoftkeySuppress(nowMs() + 250);
|
||||
hideWindow(false);
|
||||
if (!sendPluginCommand("calllog", "OPEN")) {
|
||||
@@ -675,7 +934,17 @@ int dialer_run_main(int argc, char** argv) {
|
||||
render();
|
||||
}
|
||||
} else if (comb == 2) {
|
||||
if (!number.empty()) {
|
||||
if (modal == DialerModal::Imei) {
|
||||
modal = DialerModal::Inactive;
|
||||
render();
|
||||
} else if (modal == DialerModal::Ussd) {
|
||||
if (ussdSessionActive) {
|
||||
cancelModemUssd();
|
||||
ussdSessionActive = false;
|
||||
}
|
||||
modal = DialerModal::Inactive;
|
||||
render();
|
||||
} else if (!number.empty()) {
|
||||
number.pop_back();
|
||||
selected = -1;
|
||||
updateFilter();
|
||||
|
||||
@@ -274,6 +274,13 @@ static const char *resolve_plugin_name_from_argv0(const char *argv0) {
|
||||
strcasecmp(base, "xbluetooth") == 0 || strcasecmp(base, "bluetooth") == 0) {
|
||||
return "networks";
|
||||
}
|
||||
if (strcasecmp(base, "xringtones") == 0 || strcasecmp(base, "ringtones") == 0) {
|
||||
return "ringtones";
|
||||
}
|
||||
if (strcasecmp(base, "xwallpapers") == 0 || strcasecmp(base, "wallpapers") == 0 ||
|
||||
strcasecmp(base, "xbackground") == 0 || strcasecmp(base, "background") == 0) {
|
||||
return "wallpapers";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ void handleSignal(int) {
|
||||
enum class TabId {
|
||||
SimCards = 0,
|
||||
RadioInfo = 1,
|
||||
ModemLog = 2,
|
||||
AtConsole = 2,
|
||||
ModemLog = 3,
|
||||
};
|
||||
|
||||
enum class ModalMode {
|
||||
@@ -94,6 +95,8 @@ struct ModemStatusData {
|
||||
std::string accessTech;
|
||||
int signalQuality = 0;
|
||||
std::string imei;
|
||||
std::string imei2;
|
||||
std::string imeiSv;
|
||||
std::string ownNumber;
|
||||
std::string simDbusPath;
|
||||
std::string simIccid;
|
||||
@@ -126,18 +129,42 @@ struct ModemLogRow {
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
std::string shellQuote(const std::string& s) {
|
||||
std::string out = "'";
|
||||
for (char c : s) {
|
||||
if (c == '\'') {
|
||||
out += "'\\''";
|
||||
} else {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
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,
|
||||
@@ -231,6 +258,11 @@ ModemStatusData queryModemAndSimStatus() {
|
||||
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"]);
|
||||
@@ -452,6 +484,18 @@ int runModemApp(int argc, char** argv) {
|
||||
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;
|
||||
|
||||
@@ -517,6 +561,8 @@ int runModemApp(int argc, char** argv) {
|
||||
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;
|
||||
@@ -604,9 +650,9 @@ int runModemApp(int argc, char** argv) {
|
||||
XSetForeground(dpy, gc, pt.spec.border);
|
||||
XDrawLine(dpy, pix, gc, 0, 29, kWinWidth, 29);
|
||||
|
||||
const char* tabLabels[3] = {"1:SIMs", "2:Radio", "3:Log"};
|
||||
int tabW = kWinWidth / 3;
|
||||
for (int t = 0; t < 3; ++t) {
|
||||
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);
|
||||
@@ -742,12 +788,11 @@ int runModemApp(int argc, char** argv) {
|
||||
(status.primaryGwSlot > 0 ? std::to_string(status.primaryGwSlot) : "--");
|
||||
drawPango(layoutSmall, &colLight, 10, contentY + 42, 18, sigLine, 220);
|
||||
|
||||
std::string imeiLine = "IMEI: " + (status.imei.empty() ? "--" : status.imei);
|
||||
drawPango(layoutSmall, &colMuted, 10, contentY + 60, 18, imeiLine, 220);
|
||||
std::string imei1Line = "IMEI 1: " + (status.imei.empty() ? "--" : status.imei);
|
||||
drawPango(layoutSmall, &colMuted, 10, contentY + 60, 18, imei1Line, 220);
|
||||
|
||||
std::string iccidLine = "ICCID: " +
|
||||
(status.simIccid.empty() ? "--" : status.simIccid);
|
||||
drawPango(layoutSmall, &colMuted, 10, contentY + 78, 18, iccidLine, 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;
|
||||
@@ -772,6 +817,75 @@ int runModemApp(int argc, char** argv) {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -1144,16 +1258,17 @@ int runModemApp(int argc, char** argv) {
|
||||
}
|
||||
} else if (modal == ModalMode::UssdDialog) {
|
||||
if (ussdInput.empty()) return;
|
||||
ussdResponse = "Sending USSD request...";
|
||||
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();
|
||||
std::string cmd = ussdWaitingReply
|
||||
? ("mmcli -m any --3gpp-ussd-respond=" + shellQuote(ussdInput) + " 2>&1")
|
||||
: ("mmcli -m any --3gpp-ussd-initiate=" + shellQuote(ussdInput) + " 2>&1");
|
||||
int rc = 0;
|
||||
std::string out = trimStr(runCaptureCmd(cmd, &rc));
|
||||
ussdResponse = out.empty() ? "(No response)" : out;
|
||||
std::string stOut = runCaptureCmd("mmcli -m any --3gpp-ussd-status -K 2>/dev/null");
|
||||
ussdWaitingReply = (stOut.find("user-response") != std::string::npos);
|
||||
ussdResponse = sendModemUssd(ussdInput, ussdWaitingReply, &ussdWaitingReply);
|
||||
if (ussdWaitingReply) {
|
||||
ussdInput.clear();
|
||||
}
|
||||
@@ -1212,6 +1327,30 @@ int runModemApp(int argc, char** argv) {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -1256,6 +1395,8 @@ int runModemApp(int argc, char** argv) {
|
||||
} 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");
|
||||
@@ -1312,6 +1453,12 @@ int runModemApp(int argc, char** argv) {
|
||||
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())) {
|
||||
@@ -1474,8 +1621,84 @@ int runModemApp(int argc, char** argv) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Main View Navigation
|
||||
// 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();
|
||||
@@ -1485,25 +1708,39 @@ int runModemApp(int argc, char** argv) {
|
||||
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) + 2) % 3);
|
||||
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) % 3);
|
||||
activeTab = static_cast<TabId>((static_cast<int>(activeTab) + 1) % 4);
|
||||
if (activeTab == TabId::ModemLog) refreshData(true);
|
||||
}
|
||||
render();
|
||||
@@ -1512,6 +1749,12 @@ int runModemApp(int argc, char** argv) {
|
||||
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);
|
||||
}
|
||||
@@ -1521,11 +1764,19 @@ int runModemApp(int argc, char** argv) {
|
||||
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) {
|
||||
} else if (sym == XK_Return || sym == XK_KP_Enter ||
|
||||
sym == XF86XK_Send || sym == XF86XK_Phone ||
|
||||
sym == XF86XK_PickupPhone) {
|
||||
handleEnterKey();
|
||||
render();
|
||||
}
|
||||
@@ -1565,7 +1816,8 @@ int runModemApp(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
// Periodic 5-second status refresh while visible on SIM/Radio tabs
|
||||
if (modal == ModalMode::None && activeTab != TabId::ModemLog) {
|
||||
if (modal == ModalMode::None && activeTab != TabId::ModemLog &&
|
||||
activeTab != TabId::AtConsole) {
|
||||
int64_t rRem = (lastRefreshMs + 5000) - now;
|
||||
if (rRem <= 0) {
|
||||
refreshData(false);
|
||||
|
||||
@@ -82,19 +82,6 @@ void popUtf8Char(std::string& s) {
|
||||
if (!s.empty()) s.pop_back();
|
||||
}
|
||||
|
||||
std::string shellQuote(const std::string& s) {
|
||||
std::string out = "'";
|
||||
for (char c : s) {
|
||||
if (c == '\'') {
|
||||
out += "'\\''";
|
||||
} else {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
|
||||
struct WifiApEntry {
|
||||
std::string ssid;
|
||||
int signal = 0;
|
||||
@@ -254,6 +241,12 @@ CellularNetInfo queryCellularInfo() {
|
||||
info.usbShareActive = true;
|
||||
}
|
||||
}
|
||||
if (!info.usbShareActive) {
|
||||
struct stat st {};
|
||||
if (::stat("/tmp/bananarchy_usb_tether.active", &st) == 0) {
|
||||
info.usbShareActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.gsmConfigured && !info.gsmConName.empty()) {
|
||||
std::string det = runCaptureCmd(
|
||||
@@ -433,7 +426,6 @@ int runNetworksApp(int argc, char** argv) {
|
||||
auto& colGreen = pt.colGreen;
|
||||
auto& colBlue = pt.colBlue;
|
||||
auto& colOrange = pt.colOrange;
|
||||
auto& colRed = pt.colRed;
|
||||
|
||||
NetTab activeTab = NetTab::Wifi;
|
||||
NetModal modal = NetModal::None;
|
||||
@@ -750,7 +742,7 @@ int runNetworksApp(int argc, char** argv) {
|
||||
drawPango(layoutSmall, isSel ? &colWhite : &colLight,
|
||||
10, ry, 30, rows[r], kWinWidth - 20);
|
||||
}
|
||||
drawSoftkeyBar("Refresh", "Toggle/Edit", "Close");
|
||||
drawSoftkeyBar("Refresh", "Select", "Close");
|
||||
} else if (activeTab == NetTab::Bluetooth) {
|
||||
// Row 0: BT Power Toggle (height 28)
|
||||
bool sel0 = (btRowSel == 0);
|
||||
@@ -1174,15 +1166,13 @@ int runNetworksApp(int argc, char** argv) {
|
||||
if (cellInfo.usbShareActive) {
|
||||
setBanner("Stopping USB Tethering...");
|
||||
render();
|
||||
runCaptureCmd("nmcli connection delete USBNetworkShare >/dev/null 2>&1");
|
||||
runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection usb-tether off >/dev/null 2>&1");
|
||||
refreshCurrentTab(false);
|
||||
setBanner("USB Tethering stopped");
|
||||
} else {
|
||||
setBanner("Starting USB Tethering...");
|
||||
render();
|
||||
runCaptureCmd("nmcli connection add type ethernet ifname usb0 "
|
||||
"con-name USBNetworkShare ipv4.method shared >/dev/null 2>&1 && "
|
||||
"nmcli connection up USBNetworkShare >/dev/null 2>&1");
|
||||
runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection usb-tether on >/dev/null 2>&1");
|
||||
refreshCurrentTab(false);
|
||||
setBanner("USB Tethering active");
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <fstream>
|
||||
#include <poll.h>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <unordered_map>
|
||||
@@ -88,7 +90,8 @@ constexpr const char* kIconSliders = "\xef\x87\x9e"; // U+F1DE sliders / con
|
||||
constexpr const char* kIconToggleOn = "\xef\x88\x85"; // U+F205 toggle on
|
||||
constexpr const char* kIconToggleOff = "\xef\x88\x84"; // U+F204 toggle off
|
||||
constexpr const char* kIconKey = "\xef\x82\x84"; // U+F084 key / PIN
|
||||
constexpr const char* kIconSim = "\xef\x9f\x84"; // U+F7C4 sim card / chip
|
||||
constexpr const char* kIconSim = "\xf3\xb0\x99\x95"; // U+F0655 nf-md-sim
|
||||
constexpr const char* kIconBell = "\xef\x83\xb3"; // U+F0F3 bell / ringtone
|
||||
|
||||
inline int64_t nowMs() {
|
||||
struct timespec ts {};
|
||||
@@ -344,6 +347,19 @@ inline void runAsyncCmd(const std::string& cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string shellQuote(const std::string& s) {
|
||||
std::string out = "'";
|
||||
for (char c : s) {
|
||||
if (c == '\'') {
|
||||
out += "'\\''";
|
||||
} else {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::string runCaptureCmd(const std::string& cmd, int* exitCodeOut = nullptr) {
|
||||
FILE* fp = ::popen(cmd.c_str(), "r");
|
||||
if (!fp) {
|
||||
@@ -1046,6 +1062,8 @@ inline void closeOtherPlugins(const char* selfPluginName) {
|
||||
{"xthemes.pid", "themes"},
|
||||
{"xmodem.pid", "modem"},
|
||||
{"xnetworks.pid", "networks"},
|
||||
{"xringtones.pid", "ringtones"},
|
||||
{"xwallpapers.pid", "wallpapers"},
|
||||
};
|
||||
for (const auto& p : kAllPlugins) {
|
||||
if (selfPluginName && std::strcmp(p.name, selfPluginName) == 0) {
|
||||
@@ -1074,6 +1092,8 @@ inline void broadcastThemeReload(const char* selfPluginName = nullptr) {
|
||||
"themes",
|
||||
"modem",
|
||||
"networks",
|
||||
"ringtones",
|
||||
"wallpapers",
|
||||
};
|
||||
for (const char* name : kPlugins) {
|
||||
if (selfPluginName && std::strcmp(name, selfPluginName) == 0) {
|
||||
@@ -1355,6 +1375,260 @@ inline std::vector<SmsThread> loadSmsThreads(const ContactBook& book) {
|
||||
return threads;
|
||||
}
|
||||
|
||||
struct DeviceImeis {
|
||||
std::string imei1;
|
||||
std::string imei2;
|
||||
std::string imeiSv;
|
||||
};
|
||||
|
||||
inline bool queryQrtrDmsImeis(DeviceImeis& out) {
|
||||
constexpr int kAfQipcrtr = 42;
|
||||
constexpr uint32_t kQrtrPortCtrl = 0xfffffffeu;
|
||||
constexpr uint32_t kQrtrTypeNewServer = 4;
|
||||
constexpr uint32_t kQrtrTypeNewLookup = 10;
|
||||
constexpr uint32_t kQmiServiceDms = 2;
|
||||
|
||||
struct SockAddrQrtr {
|
||||
unsigned short sq_family;
|
||||
uint32_t sq_node;
|
||||
uint32_t sq_port;
|
||||
};
|
||||
|
||||
struct QrtrCtrlPkt {
|
||||
uint32_t cmd;
|
||||
uint32_t service;
|
||||
uint32_t instance;
|
||||
uint32_t node;
|
||||
uint32_t port;
|
||||
} __attribute__((packed));
|
||||
|
||||
int fd = ::socket(kAfQipcrtr, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0) return false;
|
||||
|
||||
SockAddrQrtr localSq {};
|
||||
socklen_t sl = sizeof(localSq);
|
||||
if (::getsockname(fd, reinterpret_cast<struct sockaddr*>(&localSq), &sl) < 0) {
|
||||
::close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
SockAddrQrtr ctrlSq {};
|
||||
ctrlSq.sq_family = kAfQipcrtr;
|
||||
ctrlSq.sq_node = localSq.sq_node;
|
||||
ctrlSq.sq_port = kQrtrPortCtrl;
|
||||
|
||||
QrtrCtrlPkt lookup {};
|
||||
lookup.cmd = kQrtrTypeNewLookup;
|
||||
lookup.service = kQmiServiceDms;
|
||||
lookup.instance = 0;
|
||||
|
||||
if (::sendto(fd, &lookup, sizeof(lookup), 0,
|
||||
reinterpret_cast<const struct sockaddr*>(&ctrlSq),
|
||||
sizeof(ctrlSq)) < 0) {
|
||||
::close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t dmsNode = 0;
|
||||
uint32_t dmsPort = 0;
|
||||
bool foundDms = false;
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
struct pollfd pfd { fd, POLLIN, 0 };
|
||||
if (::poll(&pfd, 1, 60) <= 0) break;
|
||||
QrtrCtrlPkt pkt {};
|
||||
ssize_t n = ::recv(fd, &pkt, sizeof(pkt), 0);
|
||||
if (n >= static_cast<ssize_t>(sizeof(pkt)) &&
|
||||
pkt.cmd == kQrtrTypeNewServer) {
|
||||
if (pkt.service == kQmiServiceDms && pkt.port != 0) {
|
||||
dmsNode = pkt.node;
|
||||
dmsPort = pkt.port;
|
||||
foundDms = true;
|
||||
break;
|
||||
}
|
||||
if (pkt.service == 0 && pkt.node == 0 && pkt.port == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
::close(fd);
|
||||
if (!foundDms) return false;
|
||||
|
||||
auto querySubImei = [&](uint32_t subType, std::string& imeiOut,
|
||||
std::string& svOut) {
|
||||
int sfd = ::socket(kAfQipcrtr, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||
if (sfd < 0) return;
|
||||
|
||||
SockAddrQrtr dmsSq {};
|
||||
dmsSq.sq_family = kAfQipcrtr;
|
||||
dmsSq.sq_node = dmsNode;
|
||||
dmsSq.sq_port = dmsPort;
|
||||
|
||||
// 1. QMI_DMS_BIND_SUBSCRIPTION (0x0057), TLV 0x01 = uint32_t(subType)
|
||||
if (subType > 0) {
|
||||
uint8_t bindReq[14] = {
|
||||
0x00, 0x01, 0x00, // flags=0 (req), txn_id=1
|
||||
0x57, 0x00, // msg_id = 0x0057
|
||||
0x07, 0x00, // tlv_len = 7
|
||||
0x01, 0x04, 0x00, // TLV 0x01, len 4
|
||||
static_cast<uint8_t>(subType & 0xFF), 0x00, 0x00, 0x00
|
||||
};
|
||||
::sendto(sfd, bindReq, sizeof(bindReq), 0,
|
||||
reinterpret_cast<const struct sockaddr*>(&dmsSq),
|
||||
sizeof(dmsSq));
|
||||
struct pollfd pfd { sfd, POLLIN, 0 };
|
||||
if (::poll(&pfd, 1, 60) > 0) {
|
||||
uint8_t discard[256];
|
||||
::recv(sfd, discard, sizeof(discard), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. QMI_DMS_GET_DEVICE_SERIAL_NUMBERS (0x0025)
|
||||
uint8_t getIdsReq[7] = {
|
||||
0x00, 0x02, 0x00, // flags=0 (req), txn_id=2
|
||||
0x25, 0x00, // msg_id = 0x0025
|
||||
0x00, 0x00 // tlv_len = 0
|
||||
};
|
||||
if (::sendto(sfd, getIdsReq, sizeof(getIdsReq), 0,
|
||||
reinterpret_cast<const struct sockaddr*>(&dmsSq),
|
||||
sizeof(dmsSq)) >= 0) {
|
||||
struct pollfd pfd { sfd, POLLIN, 0 };
|
||||
if (::poll(&pfd, 1, 80) > 0) {
|
||||
uint8_t buf[512];
|
||||
ssize_t rlen = ::recv(sfd, buf, sizeof(buf), 0);
|
||||
if (rlen >= 7 && buf[0] == 0x02 &&
|
||||
buf[3] == 0x25 && buf[4] == 0x00) {
|
||||
size_t tlvLen = static_cast<size_t>(buf[5]) |
|
||||
(static_cast<size_t>(buf[6]) << 8);
|
||||
size_t pos = 7;
|
||||
size_t end = std::min(static_cast<size_t>(rlen), 7 + tlvLen);
|
||||
while (pos + 3 <= end) {
|
||||
uint8_t tType = buf[pos];
|
||||
size_t tLen = static_cast<size_t>(buf[pos + 1]) |
|
||||
(static_cast<size_t>(buf[pos + 2]) << 8);
|
||||
pos += 3;
|
||||
if (pos + tLen > end) break;
|
||||
if (tType == 0x11 && tLen > 0) {
|
||||
imeiOut.assign(reinterpret_cast<const char*>(buf + pos), tLen);
|
||||
imeiOut = trimStr(imeiOut);
|
||||
} else if (tType == 0x13 && tLen > 0 && svOut.empty()) {
|
||||
svOut.assign(reinterpret_cast<const char*>(buf + pos), tLen);
|
||||
svOut = trimStr(svOut);
|
||||
}
|
||||
pos += tLen;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
::close(sfd);
|
||||
};
|
||||
|
||||
querySubImei(1, out.imei1, out.imeiSv);
|
||||
if (out.imei1.empty()) {
|
||||
querySubImei(0, out.imei1, out.imeiSv);
|
||||
}
|
||||
querySubImei(2, out.imei2, out.imeiSv);
|
||||
return !out.imei1.empty() || !out.imei2.empty();
|
||||
}
|
||||
|
||||
inline DeviceImeis queryDeviceImeis() {
|
||||
static DeviceImeis cached;
|
||||
if (!cached.imei1.empty() && !cached.imei2.empty()) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
DeviceImeis res = cached;
|
||||
queryQrtrDmsImeis(res);
|
||||
|
||||
if (res.imei1.empty() || res.imei2.empty()) {
|
||||
std::string helperOut = runCaptureCmd(
|
||||
"doas -n /usr/libexec/msm-modem-uim-selection imei 2>/dev/null");
|
||||
std::istringstream ss(helperOut);
|
||||
std::string line;
|
||||
while (std::getline(ss, line)) {
|
||||
if (line.rfind("IMEI1=", 0) == 0 && res.imei1.empty()) {
|
||||
res.imei1 = trimStr(line.substr(6));
|
||||
} else if (line.rfind("IMEI2=", 0) == 0 && res.imei2.empty()) {
|
||||
res.imei2 = trimStr(line.substr(6));
|
||||
} else if (line.rfind("IMEI_SV=", 0) == 0 && res.imeiSv.empty()) {
|
||||
res.imeiSv = trimStr(line.substr(8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res.imei2.empty() && !res.imei1.empty()) {
|
||||
res.imei2 = res.imei1;
|
||||
}
|
||||
if (!res.imei1.empty()) {
|
||||
cached = res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
inline bool isUssdCode(const std::string& code) {
|
||||
std::string s = trimStr(code);
|
||||
return s.size() >= 3 && (s.front() == '*' || s.front() == '#') &&
|
||||
s.back() == '#';
|
||||
}
|
||||
|
||||
inline std::string sendModemUssd(const std::string& input,
|
||||
bool isReply = false,
|
||||
bool* outSessionActive = nullptr) {
|
||||
if (outSessionActive) *outSessionActive = false;
|
||||
std::string clean = trimStr(input);
|
||||
if (clean.empty()) return "Empty USSD request.";
|
||||
|
||||
int rc = 0;
|
||||
std::string cmd;
|
||||
if (isReply && !isUssdCode(clean)) {
|
||||
cmd = "mmcli -m any --3gpp-ussd-respond=" + shellQuote(clean) + " 2>&1";
|
||||
} else {
|
||||
cmd = "mmcli -m any --3gpp-ussd-cancel >/dev/null 2>&1; "
|
||||
"mmcli -m any --3gpp-ussd-initiate=" + shellQuote(clean) + " 2>&1";
|
||||
}
|
||||
|
||||
std::string out = trimStr(runCaptureCmd(cmd, &rc));
|
||||
size_t q1 = out.find('\'');
|
||||
size_t q2 = out.rfind('\'');
|
||||
if (q1 != std::string::npos && q2 != std::string::npos && q2 > q1) {
|
||||
out = out.substr(q1 + 1, q2 - q1 - 1);
|
||||
}
|
||||
if (out.empty()) {
|
||||
out = (rc == 0) ? "USSD request completed." : "USSD request failed.";
|
||||
}
|
||||
|
||||
if (outSessionActive) {
|
||||
std::string stOut = runCaptureCmd(
|
||||
"mmcli -m any -K 2>/dev/null | grep 'modem.3gpp.ussd.state'");
|
||||
if (stOut.find("user-response") != std::string::npos) {
|
||||
*outSessionActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Log USSD event to modemlog.tsv
|
||||
std::string logPath = getModemLogFilePath();
|
||||
ContactBook::ensureParentDir(logPath);
|
||||
std::ofstream logOut(logPath, std::ios::app);
|
||||
if (logOut.is_open()) {
|
||||
std::time_t now = std::time(nullptr);
|
||||
struct tm tmNow {};
|
||||
localtime_r(&now, &tmNow);
|
||||
char tbuf[32];
|
||||
std::strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tmNow);
|
||||
std::string oneLine = out;
|
||||
for (char& c : oneLine) {
|
||||
if (c == '\n' || c == '\r' || c == '\t') c = ' ';
|
||||
}
|
||||
logOut << tbuf << "\tussd\t" << clean << "\t" << oneLine << "\n";
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
inline void cancelModemUssd() {
|
||||
runAsyncCmd("mmcli -m any --3gpp-ussd-cancel >/dev/null 2>&1");
|
||||
}
|
||||
|
||||
} // namespace bananarchy
|
||||
|
||||
#endif // DWM_PLUGINS_PHONE_DATA_H
|
||||
|
||||
@@ -0,0 +1,858 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
// Bananarchy Ringtone & Ringer Manager Plugin (ringtones.so / xringtones)
|
||||
|
||||
#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 <dirent.h>
|
||||
#include <poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.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 {
|
||||
Ringtones = 0,
|
||||
Settings = 1,
|
||||
};
|
||||
|
||||
struct RingtoneEntry {
|
||||
std::string path;
|
||||
std::string displayName;
|
||||
std::string formatTag;
|
||||
std::string sourceTag;
|
||||
off_t sizeBytes = 0;
|
||||
};
|
||||
|
||||
struct RingSettings {
|
||||
int ringerMode = 0; // 0 = Ring + Vibrate, 1 = Ring Only, 2 = Vibrate Only, 3 = Silent
|
||||
int ringTimeSec = 20;
|
||||
int ringLoops = 10;
|
||||
int vibrateStrength = 1;
|
||||
};
|
||||
|
||||
constexpr int kRingTimeSteps[] = {10, 15, 20, 25, 30, 45, 60};
|
||||
constexpr int kNumRingTimeSteps =
|
||||
static_cast<int>(sizeof(kRingTimeSteps) / sizeof(kRingTimeSteps[0]));
|
||||
|
||||
constexpr int kRingLoopSteps[] = {3, 5, 10, 15, 20, 50};
|
||||
constexpr int kNumRingLoopSteps =
|
||||
static_cast<int>(sizeof(kRingLoopSteps) / sizeof(kRingLoopSteps[0]));
|
||||
|
||||
std::string getConfigDir() {
|
||||
const char* xdg = std::getenv("XDG_CONFIG_HOME");
|
||||
if (xdg && *xdg) {
|
||||
return std::string(xdg) + "/sxmo";
|
||||
}
|
||||
return getHomeDirPath() + "/.config/sxmo";
|
||||
}
|
||||
|
||||
bool isAudioExtension(const std::string& name, std::string* extUpperOut = nullptr) {
|
||||
size_t dot = name.rfind('.');
|
||||
if (dot == std::string::npos || dot + 1 >= name.size()) return false;
|
||||
std::string ext = toLowerStr(name.substr(dot + 1));
|
||||
if (ext == "wav" || ext == "ogg" || ext == "mp3" ||
|
||||
ext == "flac" || ext == "opus" || ext == "m4a") {
|
||||
if (extUpperOut) {
|
||||
*extUpperOut = ext;
|
||||
for (char& c : *extUpperOut) {
|
||||
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string prettifyFilename(const std::string& filename) {
|
||||
size_t dot = filename.rfind('.');
|
||||
std::string stem = (dot == std::string::npos) ? filename : filename.substr(0, dot);
|
||||
for (char& c : stem) {
|
||||
if (c == '_' || c == '-') c = ' ';
|
||||
}
|
||||
return trimStr(stem);
|
||||
}
|
||||
|
||||
void scanDirectoryForRingtones(const std::string& dirPath,
|
||||
const std::string& sourceTag,
|
||||
std::vector<RingtoneEntry>& out) {
|
||||
DIR* d = ::opendir(dirPath.c_str());
|
||||
if (!d) return;
|
||||
std::vector<std::string> names;
|
||||
while (struct dirent* ent = ::readdir(d)) {
|
||||
if (ent->d_name[0] == '.') continue;
|
||||
names.emplace_back(ent->d_name);
|
||||
}
|
||||
::closedir(d);
|
||||
std::sort(names.begin(), names.end());
|
||||
|
||||
for (const auto& name : names) {
|
||||
std::string extTag;
|
||||
if (!isAudioExtension(name, &extTag)) continue;
|
||||
std::string fullPath = dirPath + "/" + name;
|
||||
struct stat st {};
|
||||
if (::stat(fullPath.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) continue;
|
||||
RingtoneEntry e;
|
||||
e.path = fullPath;
|
||||
e.displayName = prettifyFilename(name);
|
||||
e.formatTag = extTag;
|
||||
e.sourceTag = sourceTag;
|
||||
e.sizeBytes = st.st_size;
|
||||
out.push_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<RingtoneEntry> discoverRingtones() {
|
||||
std::vector<RingtoneEntry> list;
|
||||
scanDirectoryForRingtones("/usr/share/sxmo/ringtones", "System", list);
|
||||
|
||||
struct stat st {};
|
||||
if (::stat("/usr/share/sxmo/ring.ogg", &st) == 0 && S_ISREG(st.st_mode)) {
|
||||
RingtoneEntry defEntry;
|
||||
defEntry.path = "/usr/share/sxmo/ring.ogg";
|
||||
defEntry.displayName = "Sxmo Default Ring";
|
||||
defEntry.formatTag = "OGG";
|
||||
defEntry.sourceTag = "System";
|
||||
defEntry.sizeBytes = st.st_size;
|
||||
list.push_back(defEntry);
|
||||
}
|
||||
|
||||
std::string home = getHomeDirPath();
|
||||
scanDirectoryForRingtones(home + "/Ringtones", "User", list);
|
||||
scanDirectoryForRingtones(home + "/Music/Ringtones", "User", list);
|
||||
scanDirectoryForRingtones(home + "/Music", "Music", list);
|
||||
return list;
|
||||
}
|
||||
|
||||
std::string loadActiveRingtonePath() {
|
||||
std::string cfgPath = getConfigDir() + "/ringtone";
|
||||
std::ifstream ifs(cfgPath);
|
||||
if (ifs) {
|
||||
std::string line;
|
||||
if (std::getline(ifs, line)) {
|
||||
line = trimStr(line);
|
||||
struct stat st {};
|
||||
if (!line.empty() && ::stat(line.c_str(), &st) == 0) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
const char* envRing = std::getenv("SXMO_RINGTONE");
|
||||
if (envRing && *envRing) return envRing;
|
||||
return "/usr/share/sxmo/ring.ogg";
|
||||
}
|
||||
|
||||
bool saveActiveRingtonePath(const std::string& path) {
|
||||
std::string cfgDir = getConfigDir();
|
||||
::mkdir(cfgDir.c_str(), 0755);
|
||||
std::ofstream ofs(cfgDir + "/ringtone", std::ios::trunc);
|
||||
if (!ofs) return false;
|
||||
ofs << path << "\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
RingSettings loadRingSettings() {
|
||||
RingSettings s;
|
||||
std::string cfgDir = getConfigDir();
|
||||
bool noRing = (::access((cfgDir + "/.noring").c_str(), F_OK) == 0);
|
||||
bool noVib = (::access((cfgDir + "/.novibrate").c_str(), F_OK) == 0);
|
||||
if (!noRing && !noVib) s.ringerMode = 0;
|
||||
else if (!noRing && noVib) s.ringerMode = 1;
|
||||
else if (noRing && !noVib) s.ringerMode = 2;
|
||||
else s.ringerMode = 3;
|
||||
|
||||
std::ifstream ifs(cfgDir + "/ringsettings");
|
||||
std::string line;
|
||||
while (std::getline(ifs, line)) {
|
||||
line = trimStr(line);
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
auto eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
std::string k = trimStr(line.substr(0, eq));
|
||||
std::string v = trimStr(line.substr(eq + 1));
|
||||
if (v.size() >= 2 && ((v.front() == '"' && v.back() == '"') ||
|
||||
(v.front() == '\'' && v.back() == '\''))) {
|
||||
v = v.substr(1, v.size() - 2);
|
||||
}
|
||||
int val = std::atoi(v.c_str());
|
||||
if (k == "SXMO_RINGTIME" && val >= 5 && val <= 120) {
|
||||
s.ringTimeSec = val;
|
||||
} else if (k == "SXMO_RINGNUMBER" && val >= 1 && val <= 100) {
|
||||
s.ringLoops = val;
|
||||
} else if (k == "SXMO_VIBRATE_STRENGTH" && val >= 1 && val <= 3) {
|
||||
s.vibrateStrength = val;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void saveRingSettings(const RingSettings& s) {
|
||||
std::string cfgDir = getConfigDir();
|
||||
::mkdir(cfgDir.c_str(), 0755);
|
||||
|
||||
std::string noRingPath = cfgDir + "/.noring";
|
||||
std::string noVibPath = cfgDir + "/.novibrate";
|
||||
if (s.ringerMode == 0) {
|
||||
::unlink(noRingPath.c_str());
|
||||
::unlink(noVibPath.c_str());
|
||||
} else if (s.ringerMode == 1) {
|
||||
::unlink(noRingPath.c_str());
|
||||
writeIntFile(noVibPath, 1);
|
||||
} else if (s.ringerMode == 2) {
|
||||
writeIntFile(noRingPath, 1);
|
||||
::unlink(noVibPath.c_str());
|
||||
} else {
|
||||
writeIntFile(noRingPath, 1);
|
||||
writeIntFile(noVibPath, 1);
|
||||
}
|
||||
|
||||
std::ofstream ofs(cfgDir + "/ringsettings", std::ios::trunc);
|
||||
if (ofs) {
|
||||
ofs << "SXMO_RINGTIME=" << s.ringTimeSec << "\n";
|
||||
ofs << "SXMO_RINGNUMBER=" << s.ringLoops << "\n";
|
||||
ofs << "SXMO_VIBRATE_STRENGTH=" << s.vibrateStrength << "\n";
|
||||
}
|
||||
runAsyncCmd("sxmo_hook_statusbar.sh audio >/dev/null 2>&1");
|
||||
}
|
||||
|
||||
std::string formatFileSize(off_t bytes) {
|
||||
if (bytes < 1024) return std::to_string(bytes) + " B";
|
||||
return std::to_string((bytes + 512) / 1024) + " KB";
|
||||
}
|
||||
|
||||
int runRingtonesApp(int argc, char** argv) {
|
||||
gRunning = 1;
|
||||
std::setlocale(LC_ALL, "");
|
||||
XSetLocaleModifiers("");
|
||||
|
||||
bool isPreload = false;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (argv[i] && std::strcmp(argv[i], "--preload") == 0) {
|
||||
isPreload = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string daemonPidPath = getPluginDaemonPidPath("ringtones");
|
||||
std::string activePidPath = getRuntimeFile("xringtones.pid");
|
||||
|
||||
if (isPluginDaemonAlive("ringtones")) {
|
||||
if (!isPreload) {
|
||||
sendPluginCommand("ringtones", "OPEN");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
writeIntFile(daemonPidPath, static_cast<int>(::getpid()));
|
||||
int fifoFd = setupPluginDaemonFifo("ringtones");
|
||||
|
||||
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[] = "xringtones";
|
||||
char resClass[] = "XRingtones";
|
||||
ch.res_name = resName;
|
||||
ch.res_class = resClass;
|
||||
XSetClassHint(dpy, win, &ch);
|
||||
XStoreName(dpy, win, "Ringtones");
|
||||
|
||||
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& colWhite = pt.colWhite;
|
||||
auto& colLight = pt.colLight;
|
||||
auto& colMuted = pt.colMuted;
|
||||
auto& colYellow = pt.colYellow;
|
||||
auto& colGreen = pt.colGreen;
|
||||
auto& colBlue = pt.colBlue;
|
||||
|
||||
TabId activeTab = TabId::Ringtones;
|
||||
std::vector<RingtoneEntry> tones;
|
||||
std::string activePath;
|
||||
RingSettings settings;
|
||||
int selIdx = 0;
|
||||
int settingsRow = 0;
|
||||
pid_t previewPid = -1;
|
||||
std::string previewPath;
|
||||
|
||||
std::string statusBanner;
|
||||
int64_t statusBannerUntilMs = 0;
|
||||
|
||||
bool visible = false;
|
||||
int64_t startupIgnoreUntilMs = 0;
|
||||
bool initialSoftkeysReleased = true;
|
||||
int softkeyPending = 0;
|
||||
int64_t softkeyDeadlineMs = 0;
|
||||
|
||||
auto setBanner = [&](const std::string& msg, int durationMs = 2200) {
|
||||
statusBanner = msg;
|
||||
statusBannerUntilMs = nowMs() + durationMs;
|
||||
};
|
||||
|
||||
auto stopPreview = [&]() {
|
||||
if (previewPid > 0) {
|
||||
::kill(previewPid, SIGTERM);
|
||||
::waitpid(previewPid, nullptr, WNOHANG);
|
||||
previewPid = -1;
|
||||
}
|
||||
previewPath.clear();
|
||||
};
|
||||
|
||||
auto checkPreviewAlive = [&]() {
|
||||
if (previewPid > 0) {
|
||||
int st = 0;
|
||||
pid_t r = ::waitpid(previewPid, &st, WNOHANG);
|
||||
if (r == previewPid || (r < 0 && errno == ECHILD)) {
|
||||
previewPid = -1;
|
||||
previewPath.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
auto togglePreview = [&](const std::string& path) {
|
||||
checkPreviewAlive();
|
||||
if (previewPid > 0 && previewPath == path) {
|
||||
stopPreview();
|
||||
setBanner("Preview stopped");
|
||||
return;
|
||||
}
|
||||
stopPreview();
|
||||
pid_t pid = ::fork();
|
||||
if (pid == 0) {
|
||||
int devNull = ::open("/dev/null", O_RDWR);
|
||||
if (devNull >= 0) {
|
||||
::dup2(devNull, STDIN_FILENO);
|
||||
::dup2(devNull, STDOUT_FILENO);
|
||||
::dup2(devNull, STDERR_FILENO);
|
||||
if (devNull > 2) ::close(devNull);
|
||||
}
|
||||
::execlp("mpv", "mpv", "--no-resume-playback", "--quiet",
|
||||
"--no-video", "--loop=2", path.c_str(), nullptr);
|
||||
::_exit(127);
|
||||
} else if (pid > 0) {
|
||||
previewPid = pid;
|
||||
previewPath = path;
|
||||
setBanner("Playing preview...");
|
||||
}
|
||||
};
|
||||
|
||||
auto refreshData = [&]() {
|
||||
tones = discoverRingtones();
|
||||
activePath = loadActiveRingtonePath();
|
||||
settings = loadRingSettings();
|
||||
if (selIdx >= static_cast<int>(tones.size())) {
|
||||
selIdx = std::max(0, static_cast<int>(tones.size()) - 1);
|
||||
}
|
||||
};
|
||||
|
||||
auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y,
|
||||
int h, const std::string& text, int maxW = -1,
|
||||
int align = 0) {
|
||||
if (text.empty()) return;
|
||||
pango_layout_set_text(layout, text.c_str(), -1);
|
||||
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);
|
||||
}
|
||||
int tw = 0, th = 0;
|
||||
pango_layout_get_pixel_size(layout, &tw, &th);
|
||||
int tx = x;
|
||||
if (align == 1 && maxW > 0) tx = x + (maxW - tw) / 2;
|
||||
else if (align == 2 && maxW > 0) tx = x + maxW - tw;
|
||||
int ty = y + (h - th) / 2;
|
||||
pango_xft_render_layout(xftDraw, color, layout, tx * PANGO_SCALE,
|
||||
ty * PANGO_SCALE);
|
||||
};
|
||||
|
||||
auto drawSoftkeyBar = [&](const std::string& leftLabel,
|
||||
const std::string& midLabel,
|
||||
const std::string& rightLabel) {
|
||||
int barY = kWinHeight - 24;
|
||||
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||||
XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 24);
|
||||
XSetForeground(dpy, gc, pt.spec.border);
|
||||
XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY);
|
||||
|
||||
drawPango(layoutSmall, &colLight, 6, barY, 24, leftLabel, 76, 0);
|
||||
drawPango(layoutMain, &colYellow, 82, barY, 24, midLabel, 76, 1);
|
||||
drawPango(layoutSmall, &colLight, 158, barY, 24, rightLabel, 76, 2);
|
||||
};
|
||||
|
||||
auto render = [&]() {
|
||||
if (!visible) return;
|
||||
checkPreviewAlive();
|
||||
|
||||
XSetForeground(dpy, gc, pt.spec.bgMain);
|
||||
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
|
||||
|
||||
// Top tab header (0..26)
|
||||
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||||
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 26);
|
||||
|
||||
const char* tabNames[2] = {"1:Ringtones", "2:Settings"};
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
int tx = i * 120;
|
||||
bool isAct = (static_cast<int>(activeTab) == i);
|
||||
if (isAct) {
|
||||
XSetForeground(dpy, gc, pt.spec.selBg);
|
||||
XFillRectangle(dpy, pix, gc, tx, 0, 120, 26);
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XFillRectangle(dpy, pix, gc, tx, 24, 120, 2);
|
||||
}
|
||||
drawPango(layoutSmall, isAct ? &colWhite : &colMuted, tx + 4, 0, 24,
|
||||
tabNames[i], 112, 1);
|
||||
}
|
||||
|
||||
if (activeTab == TabId::Ringtones) {
|
||||
// Active ringtone summary banner (y=28..50)
|
||||
XSetForeground(dpy, gc, pt.spec.bgAltEven);
|
||||
XFillRectangle(dpy, pix, gc, 4, 28, kWinWidth - 8, 22);
|
||||
std::string actName = prettifyFilename(
|
||||
activePath.substr(activePath.rfind('/') + 1));
|
||||
drawPango(layoutSmall, &colGreen, 8, 28, 22,
|
||||
std::string(kIconBell) + " Active: " + actName,
|
||||
kWinWidth - 16);
|
||||
|
||||
// List area (y=52..246, 5 rows of 38px)
|
||||
constexpr int kVisibleRows = 5;
|
||||
int total = static_cast<int>(tones.size());
|
||||
int startIdx = 0;
|
||||
if (total > kVisibleRows) {
|
||||
startIdx = std::clamp(selIdx - kVisibleRows / 2, 0,
|
||||
total - kVisibleRows);
|
||||
}
|
||||
|
||||
for (int i = 0; i < kVisibleRows; ++i) {
|
||||
int idx = startIdx + i;
|
||||
if (idx >= total) break;
|
||||
const auto& e = tones[idx];
|
||||
int ry = 54 + i * 38;
|
||||
bool isSel = (idx == selIdx);
|
||||
bool isActive = (e.path == activePath);
|
||||
bool isPlaying = (previewPid > 0 && previewPath == e.path);
|
||||
|
||||
XSetForeground(dpy, gc, isSel ? pt.spec.selBg
|
||||
: (i % 2 == 0 ? pt.spec.bgAltEven
|
||||
: pt.spec.bgAltOdd));
|
||||
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 36);
|
||||
if (isActive) {
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XFillRectangle(dpy, pix, gc, 4, ry, 3, 36);
|
||||
}
|
||||
|
||||
drawPango(layoutMain, isSel ? &colWhite : &colLight, 10, ry + 1, 19,
|
||||
e.displayName, 152);
|
||||
|
||||
std::string badge;
|
||||
XftColor* badgeCol = &colMuted;
|
||||
if (isPlaying) {
|
||||
badge = "[PLAYING]";
|
||||
badgeCol = &colYellow;
|
||||
} else if (isActive) {
|
||||
badge = "[ACTIVE]";
|
||||
badgeCol = &colGreen;
|
||||
}
|
||||
if (!badge.empty()) {
|
||||
drawPango(layoutSmall, badgeCol, 162, ry + 2, 18, badge, 70, 2);
|
||||
}
|
||||
|
||||
std::string sub = e.formatTag + " | " + formatFileSize(e.sizeBytes) +
|
||||
" | " + e.sourceTag + " ([*] Preview)";
|
||||
drawPango(layoutSmall, isSel ? &colLight : &colMuted, 10, ry + 19, 16,
|
||||
sub, kWinWidth - 20);
|
||||
}
|
||||
|
||||
bool curPlaying = (!tones.empty() && selIdx >= 0 &&
|
||||
selIdx < static_cast<int>(tones.size()) &&
|
||||
previewPid > 0 &&
|
||||
previewPath == tones[selIdx].path);
|
||||
drawSoftkeyBar(curPlaying ? "Stop" : "Play [*]", "Set Ring", "Close");
|
||||
} else {
|
||||
// Tab 2: Settings
|
||||
const char* modeLabels[4] = {
|
||||
"Ring + Vibrate",
|
||||
"Ring Only",
|
||||
"Vibrate Only",
|
||||
"Silent",
|
||||
};
|
||||
std::string rows[5][2] = {
|
||||
{"Ringer Mode",
|
||||
"< " + std::string(modeLabels[settings.ringerMode % 4]) + " >"},
|
||||
{"Ring Duration",
|
||||
"< " + std::to_string(settings.ringTimeSec) + " sec >"},
|
||||
{"Ring Loop Count",
|
||||
"< " + std::to_string(settings.ringLoops) + " loops >"},
|
||||
{"Vibrate Strength",
|
||||
"< Level " + std::to_string(settings.vibrateStrength) + " >"},
|
||||
{"Test Vibration", "Press OK to vibrate"},
|
||||
};
|
||||
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
int ry = 32 + i * 42;
|
||||
bool isSel = (settingsRow == i);
|
||||
XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgAltEven);
|
||||
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 38);
|
||||
if (isSel) {
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XDrawRectangle(dpy, pix, gc, 4, ry, kWinWidth - 9, 37);
|
||||
}
|
||||
drawPango(layoutSmall, isSel ? &colWhite : &colMuted, 10, ry + 2, 17,
|
||||
rows[i][0], kWinWidth - 20);
|
||||
drawPango(layoutMain, isSel ? &colYellow : &colBlue, 10, ry + 18, 19,
|
||||
rows[i][1], kWinWidth - 20);
|
||||
}
|
||||
drawSoftkeyBar("Change", "Select", "Close");
|
||||
}
|
||||
|
||||
if (!statusBanner.empty()) {
|
||||
int by = kWinHeight - 46;
|
||||
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||||
XFillRectangle(dpy, pix, gc, 6, by, kWinWidth - 12, 20);
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XDrawRectangle(dpy, pix, gc, 6, by, kWinWidth - 13, 19);
|
||||
drawPango(layoutSmall, &colYellow, 10, by, 20, statusBanner,
|
||||
kWinWidth - 20, 1);
|
||||
}
|
||||
|
||||
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
|
||||
XFlush(dpy);
|
||||
};
|
||||
|
||||
auto showWindow = [&]() {
|
||||
closeOtherPlugins("ringtones");
|
||||
refreshData();
|
||||
for (size_t i = 0; i < tones.size(); ++i) {
|
||||
if (tones[i].path == activePath) {
|
||||
selIdx = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
writeIntFile(activePidPath, static_cast<int>(::getpid()));
|
||||
visible = true;
|
||||
startupIgnoreUntilMs = nowMs() + 180;
|
||||
initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
|
||||
softkeyPending = 0;
|
||||
XMapRaised(dpy, win);
|
||||
XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime);
|
||||
render();
|
||||
};
|
||||
|
||||
auto hideWindow = [&]() {
|
||||
stopPreview();
|
||||
if (!visible) return;
|
||||
visible = false;
|
||||
softkeyPending = 0;
|
||||
XUnmapWindow(dpy, win);
|
||||
::unlink(activePidPath.c_str());
|
||||
XFlush(dpy);
|
||||
};
|
||||
|
||||
auto adjustSettingRow = [&](int delta) {
|
||||
if (settingsRow == 0) {
|
||||
settings.ringerMode = (settings.ringerMode + 4 + delta) % 4;
|
||||
saveRingSettings(settings);
|
||||
setBanner("Ringer mode updated");
|
||||
} else if (settingsRow == 1) {
|
||||
int curIdx = 2;
|
||||
for (int i = 0; i < kNumRingTimeSteps; ++i) {
|
||||
if (kRingTimeSteps[i] == settings.ringTimeSec) {
|
||||
curIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
curIdx = (curIdx + kNumRingTimeSteps + delta) % kNumRingTimeSteps;
|
||||
settings.ringTimeSec = kRingTimeSteps[curIdx];
|
||||
saveRingSettings(settings);
|
||||
} else if (settingsRow == 2) {
|
||||
int curIdx = 2;
|
||||
for (int i = 0; i < kNumRingLoopSteps; ++i) {
|
||||
if (kRingLoopSteps[i] == settings.ringLoops) {
|
||||
curIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
curIdx = (curIdx + kNumRingLoopSteps + delta) % kNumRingLoopSteps;
|
||||
settings.ringLoops = kRingLoopSteps[curIdx];
|
||||
saveRingSettings(settings);
|
||||
} else if (settingsRow == 3) {
|
||||
settings.vibrateStrength =
|
||||
((settings.vibrateStrength - 1 + 3 + delta) % 3) + 1;
|
||||
saveRingSettings(settings);
|
||||
} else if (settingsRow == 4) {
|
||||
runAsyncCmd("sxmo_vibrate 600 " +
|
||||
std::to_string(settings.vibrateStrength) +
|
||||
" >/dev/null 2>&1");
|
||||
setBanner("Testing vibration...");
|
||||
}
|
||||
};
|
||||
|
||||
if (!isPreload) {
|
||||
showWindow();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
char dch = mapDialKey(sym, kc, ev.xkey.state);
|
||||
if (dch == '1') {
|
||||
activeTab = TabId::Ringtones;
|
||||
render();
|
||||
continue;
|
||||
} else if (dch == '2') {
|
||||
activeTab = TabId::Settings;
|
||||
render();
|
||||
continue;
|
||||
} else if (dch == '*' && activeTab == TabId::Ringtones &&
|
||||
!tones.empty()) {
|
||||
togglePreview(tones[selIdx].path);
|
||||
render();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sym == XK_Left || sym == XK_KP_Left) {
|
||||
if (activeTab == TabId::Settings && settingsRow < 4) {
|
||||
adjustSettingRow(-1);
|
||||
} else {
|
||||
activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings
|
||||
: TabId::Ringtones;
|
||||
}
|
||||
render();
|
||||
} else if (sym == XK_Right || sym == XK_KP_Right) {
|
||||
if (activeTab == TabId::Settings && settingsRow < 4) {
|
||||
adjustSettingRow(1);
|
||||
} else {
|
||||
activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings
|
||||
: TabId::Ringtones;
|
||||
}
|
||||
render();
|
||||
} else if (sym == XK_Up || sym == XK_KP_Up) {
|
||||
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
||||
selIdx = (selIdx + static_cast<int>(tones.size()) - 1) %
|
||||
static_cast<int>(tones.size());
|
||||
} else if (activeTab == TabId::Settings) {
|
||||
settingsRow = (settingsRow + 4) % 5;
|
||||
}
|
||||
render();
|
||||
} else if (sym == XK_Down || sym == XK_KP_Down) {
|
||||
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
||||
selIdx = (selIdx + 1) % static_cast<int>(tones.size());
|
||||
} else if (activeTab == TabId::Settings) {
|
||||
settingsRow = (settingsRow + 1) % 5;
|
||||
}
|
||||
render();
|
||||
} else if (sym == XK_Return || sym == XK_KP_Enter) {
|
||||
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
||||
activePath = tones[selIdx].path;
|
||||
saveActiveRingtonePath(activePath);
|
||||
setBanner("Ringtone set: " + tones[selIdx].displayName);
|
||||
} else if (activeTab == TabId::Settings) {
|
||||
adjustSettingRow(1);
|
||||
}
|
||||
render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int timeoutMs = -1;
|
||||
int64_t now = nowMs();
|
||||
if (visible) {
|
||||
if (checkPreviewAlive()) {
|
||||
render();
|
||||
}
|
||||
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) {
|
||||
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
||||
togglePreview(tones[selIdx].path);
|
||||
} else if (activeTab == TabId::Settings) {
|
||||
adjustSettingRow(1);
|
||||
}
|
||||
render();
|
||||
} else if (pend == 2) {
|
||||
hideWindow();
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
if (previewPid > 0 && (timeoutMs < 0 || timeoutMs > 400)) {
|
||||
timeoutMs = 400;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
showWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideWindow();
|
||||
if (fifoFd >= 0) ::close(fifoFd);
|
||||
::unlink(daemonPidPath.c_str());
|
||||
if (xftDraw) XftDrawDestroy(xftDraw);
|
||||
XFreeGC(dpy, gc);
|
||||
XFreePixmap(dpy, pix);
|
||||
XDestroyWindow(dpy, win);
|
||||
XCloseDisplay(dpy);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pluginInit(const DwmHostApi* host) {
|
||||
if (host && host->spawn_plugin) {
|
||||
host->spawn_plugin("ringtones", "--preload");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pluginCleanup(void) {}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = {
|
||||
DWM_PLUGIN_API_VERSION,
|
||||
"ringtones",
|
||||
"Bananarchy Ringtones & Ringer Manager (240x320)",
|
||||
pluginInit,
|
||||
pluginCleanup,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
runRingtonesApp
|
||||
};
|
||||
@@ -0,0 +1,716 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
// Bananarchy Desktop Background / Wallpaper Manager Plugin (wallpapers.so / xwallpapers)
|
||||
|
||||
#include "phone_data.h"
|
||||
#include "media_utils.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 <dirent.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;
|
||||
}
|
||||
|
||||
struct WallpaperEntry {
|
||||
std::string path;
|
||||
std::string displayName;
|
||||
std::string sourceTag;
|
||||
off_t sizeBytes = 0;
|
||||
};
|
||||
|
||||
struct ScaleModeSpec {
|
||||
const char* id;
|
||||
const char* label;
|
||||
const char* fehFlag;
|
||||
};
|
||||
|
||||
constexpr ScaleModeSpec kScaleModes[] = {
|
||||
{"fill", "Fill (Crop)", "--bg-fill"},
|
||||
{"scale", "Scale (Stretch)", "--bg-scale"},
|
||||
{"max", "Fit (Max)", "--bg-max"},
|
||||
{"center", "Center (1:1)", "--bg-center"},
|
||||
{"tile", "Tile", "--bg-tile"},
|
||||
};
|
||||
constexpr int kNumScaleModes =
|
||||
static_cast<int>(sizeof(kScaleModes) / sizeof(kScaleModes[0]));
|
||||
|
||||
std::string getConfigDir() {
|
||||
const char* xdg = std::getenv("XDG_CONFIG_HOME");
|
||||
if (xdg && *xdg) {
|
||||
return std::string(xdg) + "/sxmo";
|
||||
}
|
||||
return getHomeDirPath() + "/.config/sxmo";
|
||||
}
|
||||
|
||||
bool isWallpaperExt(const std::string& name) {
|
||||
size_t dot = name.rfind('.');
|
||||
if (dot == std::string::npos || dot + 1 >= name.size()) return false;
|
||||
std::string ext = toLowerStr(name.substr(dot + 1));
|
||||
return ext == "png" || ext == "jpg" || ext == "jpeg";
|
||||
}
|
||||
|
||||
std::string prettifyWallpaperName(const std::string& filename) {
|
||||
size_t dot = filename.rfind('.');
|
||||
std::string stem = (dot == std::string::npos) ? filename : filename.substr(0, dot);
|
||||
for (char& c : stem) {
|
||||
if (c == '_' || c == '-') c = ' ';
|
||||
}
|
||||
if (!stem.empty()) {
|
||||
stem[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(stem[0])));
|
||||
}
|
||||
return trimStr(stem);
|
||||
}
|
||||
|
||||
void scanWallpaperDir(const std::string& dirPath,
|
||||
const std::string& sourceTag,
|
||||
std::vector<WallpaperEntry>& out) {
|
||||
DIR* d = ::opendir(dirPath.c_str());
|
||||
if (!d) return;
|
||||
std::vector<std::string> names;
|
||||
while (struct dirent* ent = ::readdir(d)) {
|
||||
if (ent->d_name[0] == '.') continue;
|
||||
names.emplace_back(ent->d_name);
|
||||
}
|
||||
::closedir(d);
|
||||
std::sort(names.begin(), names.end());
|
||||
|
||||
for (const auto& name : names) {
|
||||
if (!isWallpaperExt(name)) continue;
|
||||
std::string fullPath = dirPath + "/" + name;
|
||||
struct stat st {};
|
||||
if (::stat(fullPath.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) continue;
|
||||
WallpaperEntry e;
|
||||
e.path = fullPath;
|
||||
e.displayName = prettifyWallpaperName(name);
|
||||
e.sourceTag = sourceTag;
|
||||
e.sizeBytes = st.st_size;
|
||||
out.push_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<WallpaperEntry> discoverWallpapers() {
|
||||
std::vector<WallpaperEntry> list;
|
||||
scanWallpaperDir("/usr/share/wallpapers", "System", list);
|
||||
|
||||
std::string home = getHomeDirPath();
|
||||
scanWallpaperDir(home + "/Pictures/Wallpapers", "Wallpapers", list);
|
||||
scanWallpaperDir(home + "/Pictures", "Pictures", list);
|
||||
return list;
|
||||
}
|
||||
|
||||
std::string loadActiveWallpaperPath() {
|
||||
std::ifstream ifs(getConfigDir() + "/wallpaper");
|
||||
if (ifs) {
|
||||
std::string line;
|
||||
if (std::getline(ifs, line)) {
|
||||
line = trimStr(line);
|
||||
struct stat st {};
|
||||
if (!line.empty() && ::stat(line.c_str(), &st) == 0) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "/usr/share/wallpapers/bananarchy.png";
|
||||
}
|
||||
|
||||
int loadActiveScaleModeIdx() {
|
||||
std::ifstream ifs(getConfigDir() + "/wallpaper_mode");
|
||||
if (ifs) {
|
||||
std::string line;
|
||||
if (std::getline(ifs, line)) {
|
||||
line = toLowerStr(trimStr(line));
|
||||
for (int i = 0; i < kNumScaleModes; ++i) {
|
||||
if (line == kScaleModes[i].id || line == kScaleModes[i].fehFlag) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void applyAndSaveWallpaper(const std::string& path, int modeIdx) {
|
||||
modeIdx = std::clamp(modeIdx, 0, kNumScaleModes - 1);
|
||||
std::string cfgDir = getConfigDir();
|
||||
::mkdir(cfgDir.c_str(), 0755);
|
||||
{
|
||||
std::ofstream ofs(cfgDir + "/wallpaper", std::ios::trunc);
|
||||
if (ofs) ofs << path << "\n";
|
||||
}
|
||||
{
|
||||
std::ofstream ofs(cfgDir + "/wallpaper_mode", std::ios::trunc);
|
||||
if (ofs) ofs << kScaleModes[modeIdx].id << "\n";
|
||||
}
|
||||
runAsyncCmd(std::string("feh --no-fehbg ") + kScaleModes[modeIdx].fehFlag +
|
||||
" " + shellQuote(path) + " >/dev/null 2>&1");
|
||||
}
|
||||
|
||||
int runWallpapersApp(int argc, char** argv) {
|
||||
gRunning = 1;
|
||||
std::setlocale(LC_ALL, "");
|
||||
XSetLocaleModifiers("");
|
||||
|
||||
bool isPreload = false;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (argv[i] && std::strcmp(argv[i], "--preload") == 0) {
|
||||
isPreload = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string daemonPidPath = getPluginDaemonPidPath("wallpapers");
|
||||
std::string activePidPath = getRuntimeFile("xwallpapers.pid");
|
||||
|
||||
if (isPluginDaemonAlive("wallpapers")) {
|
||||
if (!isPreload) {
|
||||
sendPluginCommand("wallpapers", "OPEN");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
writeIntFile(daemonPidPath, static_cast<int>(::getpid()));
|
||||
int fifoFd = setupPluginDaemonFifo("wallpapers");
|
||||
|
||||
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[] = "xwallpapers";
|
||||
char resClass[] = "XWallpapers";
|
||||
ch.res_name = resName;
|
||||
ch.res_class = resClass;
|
||||
XSetClassHint(dpy, win, &ch);
|
||||
XStoreName(dpy, win, "Wallpapers");
|
||||
|
||||
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& colWhite = pt.colWhite;
|
||||
auto& colLight = pt.colLight;
|
||||
auto& colMuted = pt.colMuted;
|
||||
auto& colYellow = pt.colYellow;
|
||||
auto& colGreen = pt.colGreen;
|
||||
auto& colBlue = pt.colBlue;
|
||||
|
||||
std::vector<WallpaperEntry> items;
|
||||
std::string activePath;
|
||||
int scaleModeIdx = 0;
|
||||
int selIdx = 0;
|
||||
bool fullscreenPreview = false;
|
||||
|
||||
// Cached decoded image for currently selected wallpaper
|
||||
std::string cachedImgPath;
|
||||
std::vector<uint8_t> cachedRgb;
|
||||
int cachedImgW = 0;
|
||||
int cachedImgH = 0;
|
||||
bool cachedImgValid = false;
|
||||
|
||||
constexpr int kThumbW = 76;
|
||||
constexpr int kThumbH = 100;
|
||||
std::vector<uint32_t> thumbPixels(kThumbW * kThumbH, 0x111111);
|
||||
XImage* thumbXImg = XCreateImage(
|
||||
dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0,
|
||||
reinterpret_cast<char*>(thumbPixels.data()), kThumbW, kThumbH, 32, 0);
|
||||
|
||||
constexpr int kFullW = 240;
|
||||
constexpr int kFullH = 246;
|
||||
std::vector<uint32_t> fullPixels(kFullW * kFullH, 0x111111);
|
||||
XImage* fullXImg = XCreateImage(
|
||||
dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0,
|
||||
reinterpret_cast<char*>(fullPixels.data()), kFullW, kFullH, 32, 0);
|
||||
|
||||
std::string statusBanner;
|
||||
int64_t statusBannerUntilMs = 0;
|
||||
|
||||
bool visible = false;
|
||||
int64_t startupIgnoreUntilMs = 0;
|
||||
bool initialSoftkeysReleased = true;
|
||||
int softkeyPending = 0;
|
||||
int64_t softkeyDeadlineMs = 0;
|
||||
|
||||
auto setBanner = [&](const std::string& msg, int durationMs = 2200) {
|
||||
statusBanner = msg;
|
||||
statusBannerUntilMs = nowMs() + durationMs;
|
||||
};
|
||||
|
||||
auto updatePreviewCache = [&]() {
|
||||
if (items.empty() || selIdx < 0 ||
|
||||
selIdx >= static_cast<int>(items.size())) {
|
||||
cachedImgPath.clear();
|
||||
cachedImgValid = false;
|
||||
return;
|
||||
}
|
||||
const std::string& p = items[selIdx].path;
|
||||
if (p == cachedImgPath) return;
|
||||
cachedImgPath = p;
|
||||
cachedRgb.clear();
|
||||
cachedImgW = 0;
|
||||
cachedImgH = 0;
|
||||
cachedImgValid = loadImageRgb24(p, &cachedRgb, &cachedImgW, &cachedImgH);
|
||||
if (cachedImgValid) {
|
||||
scaleRgb24ToBgrxCanvas(cachedRgb.data(), cachedImgW, cachedImgH,
|
||||
thumbPixels.data(), kThumbW, kThumbH,
|
||||
pt.spec.bgHeader);
|
||||
scaleRgb24ToBgrxCanvas(cachedRgb.data(), cachedImgW, cachedImgH,
|
||||
fullPixels.data(), kFullW, kFullH,
|
||||
pt.spec.bgMain);
|
||||
} else {
|
||||
std::fill(thumbPixels.begin(), thumbPixels.end(), pt.spec.bgHeader);
|
||||
std::fill(fullPixels.begin(), fullPixels.end(), pt.spec.bgMain);
|
||||
}
|
||||
};
|
||||
|
||||
auto refreshData = [&]() {
|
||||
items = discoverWallpapers();
|
||||
activePath = loadActiveWallpaperPath();
|
||||
scaleModeIdx = loadActiveScaleModeIdx();
|
||||
if (selIdx >= static_cast<int>(items.size())) {
|
||||
selIdx = std::max(0, static_cast<int>(items.size()) - 1);
|
||||
}
|
||||
cachedImgPath.clear();
|
||||
updatePreviewCache();
|
||||
};
|
||||
|
||||
auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y,
|
||||
int h, const std::string& text, int maxW = -1,
|
||||
int align = 0) {
|
||||
if (text.empty()) return;
|
||||
pango_layout_set_text(layout, text.c_str(), -1);
|
||||
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);
|
||||
}
|
||||
int tw = 0, th = 0;
|
||||
pango_layout_get_pixel_size(layout, &tw, &th);
|
||||
int tx = x;
|
||||
if (align == 1 && maxW > 0) tx = x + (maxW - tw) / 2;
|
||||
else if (align == 2 && maxW > 0) tx = x + maxW - tw;
|
||||
int ty = y + (h - th) / 2;
|
||||
pango_xft_render_layout(xftDraw, color, layout, tx * PANGO_SCALE,
|
||||
ty * PANGO_SCALE);
|
||||
};
|
||||
|
||||
auto drawSoftkeyBar = [&](const std::string& leftLabel,
|
||||
const std::string& midLabel,
|
||||
const std::string& rightLabel) {
|
||||
int barY = kWinHeight - 24;
|
||||
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||||
XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 24);
|
||||
XSetForeground(dpy, gc, pt.spec.border);
|
||||
XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY);
|
||||
|
||||
drawPango(layoutSmall, &colLight, 6, barY, 24, leftLabel, 76, 0);
|
||||
drawPango(layoutMain, &colYellow, 82, barY, 24, midLabel, 76, 1);
|
||||
drawPango(layoutSmall, &colLight, 158, barY, 24, rightLabel, 76, 2);
|
||||
};
|
||||
|
||||
auto render = [&]() {
|
||||
if (!visible) return;
|
||||
updatePreviewCache();
|
||||
|
||||
XSetForeground(dpy, gc, pt.spec.bgMain);
|
||||
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
|
||||
|
||||
// Top Header (0..26)
|
||||
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||||
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 26);
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XFillRectangle(dpy, pix, gc, 0, 24, kWinWidth, 2);
|
||||
|
||||
std::string countTag = items.empty()
|
||||
? "0/0"
|
||||
: (std::to_string(selIdx + 1) + "/" +
|
||||
std::to_string(items.size()));
|
||||
drawPango(layoutMain, &colWhite, 8, 0, 24,
|
||||
std::string(kIconImage) + " Wallpapers", 170);
|
||||
drawPango(layoutSmall, &colYellow, 180, 0, 24, countTag, 52, 2);
|
||||
|
||||
if (fullscreenPreview && cachedImgValid && fullXImg) {
|
||||
XPutImage(dpy, pix, gc, fullXImg, 0, 0, 0, 26, kFullW, kFullH);
|
||||
drawSoftkeyBar("Exit [*]", "Apply", "Back");
|
||||
} else {
|
||||
// Top Preview & Metadata Card (y=28..134)
|
||||
XSetForeground(dpy, gc, pt.spec.bgAltEven);
|
||||
XFillRectangle(dpy, pix, gc, 4, 28, kWinWidth - 8, 106);
|
||||
XSetForeground(dpy, gc, pt.spec.border);
|
||||
XDrawRectangle(dpy, pix, gc, 4, 28, kWinWidth - 9, 105);
|
||||
|
||||
// Thumbnail frame at x=8, y=31, 76x100
|
||||
if (cachedImgValid && thumbXImg) {
|
||||
XPutImage(dpy, pix, gc, thumbXImg, 0, 0, 8, 31, kThumbW, kThumbH);
|
||||
} else {
|
||||
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||||
XFillRectangle(dpy, pix, gc, 8, 31, kThumbW, kThumbH);
|
||||
drawPango(layoutSmall, &colMuted, 8, 70, 20, "No Image", kThumbW, 1);
|
||||
}
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XDrawRectangle(dpy, pix, gc, 7, 30, kThumbW + 1, kThumbH + 1);
|
||||
|
||||
if (!items.empty() && selIdx >= 0 &&
|
||||
selIdx < static_cast<int>(items.size())) {
|
||||
const auto& cur = items[selIdx];
|
||||
bool isAct = (cur.path == activePath);
|
||||
|
||||
drawPango(layoutMain, &colWhite, 90, 31, 20, cur.displayName, 142);
|
||||
|
||||
std::string dimStr = cachedImgValid
|
||||
? (std::to_string(cachedImgW) + "x" +
|
||||
std::to_string(cachedImgH))
|
||||
: "Unknown";
|
||||
drawPango(layoutSmall, &colLight, 90, 51, 18,
|
||||
dimStr + " | " + formatFileSize(cur.sizeBytes), 142);
|
||||
|
||||
drawPango(layoutSmall, isAct ? &colGreen : &colMuted, 90, 69, 18,
|
||||
isAct ? "Status: [ACTIVE]" : ("Source: " + cur.sourceTag),
|
||||
142);
|
||||
|
||||
drawPango(layoutSmall, &colBlue, 90, 89, 18,
|
||||
std::string("Fit: < ") + kScaleModes[scaleModeIdx].label +
|
||||
" >",
|
||||
142);
|
||||
drawPango(layoutSmall, &colMuted, 90, 109, 18,
|
||||
"[*] Fullscreen [< >] Fit", 142);
|
||||
}
|
||||
|
||||
// Scrollable List (y=138..266, 4 rows of 32px)
|
||||
constexpr int kVisibleRows = 4;
|
||||
int total = static_cast<int>(items.size());
|
||||
int startIdx = 0;
|
||||
if (total > kVisibleRows) {
|
||||
startIdx = std::clamp(selIdx - kVisibleRows / 2, 0,
|
||||
total - kVisibleRows);
|
||||
}
|
||||
|
||||
for (int i = 0; i < kVisibleRows; ++i) {
|
||||
int idx = startIdx + i;
|
||||
if (idx >= total) break;
|
||||
const auto& e = items[idx];
|
||||
int ry = 138 + i * 32;
|
||||
bool isSel = (idx == selIdx);
|
||||
bool isAct = (e.path == activePath);
|
||||
|
||||
XSetForeground(dpy, gc, isSel ? pt.spec.selBg
|
||||
: (i % 2 == 0 ? pt.spec.bgAltEven
|
||||
: pt.spec.bgAltOdd));
|
||||
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 30);
|
||||
if (isAct) {
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XFillRectangle(dpy, pix, gc, 4, ry, 3, 30);
|
||||
}
|
||||
|
||||
drawPango(layoutMain, isSel ? &colWhite : &colLight, 10, ry + 5, 20,
|
||||
e.displayName, 162);
|
||||
if (isAct) {
|
||||
drawPango(layoutSmall, &colGreen, 174, ry + 6, 18, "[ACTIVE]", 58, 2);
|
||||
} else {
|
||||
drawPango(layoutSmall, &colMuted, 174, ry + 6, 18, e.sourceTag, 58, 2);
|
||||
}
|
||||
}
|
||||
|
||||
drawSoftkeyBar("View [*]", "Apply", "Close");
|
||||
}
|
||||
|
||||
if (!statusBanner.empty()) {
|
||||
int by = kWinHeight - 46;
|
||||
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||||
XFillRectangle(dpy, pix, gc, 6, by, kWinWidth - 12, 20);
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XDrawRectangle(dpy, pix, gc, 6, by, kWinWidth - 13, 19);
|
||||
drawPango(layoutSmall, &colYellow, 10, by, 20, statusBanner,
|
||||
kWinWidth - 20, 1);
|
||||
}
|
||||
|
||||
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
|
||||
XFlush(dpy);
|
||||
};
|
||||
|
||||
auto showWindow = [&]() {
|
||||
closeOtherPlugins("wallpapers");
|
||||
fullscreenPreview = false;
|
||||
refreshData();
|
||||
for (size_t i = 0; i < items.size(); ++i) {
|
||||
if (items[i].path == activePath) {
|
||||
selIdx = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
updatePreviewCache();
|
||||
writeIntFile(activePidPath, static_cast<int>(::getpid()));
|
||||
visible = true;
|
||||
startupIgnoreUntilMs = nowMs() + 180;
|
||||
initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
|
||||
softkeyPending = 0;
|
||||
XMapRaised(dpy, win);
|
||||
XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime);
|
||||
render();
|
||||
};
|
||||
|
||||
auto hideWindow = [&]() {
|
||||
if (!visible) return;
|
||||
visible = false;
|
||||
fullscreenPreview = false;
|
||||
softkeyPending = 0;
|
||||
XUnmapWindow(dpy, win);
|
||||
::unlink(activePidPath.c_str());
|
||||
XFlush(dpy);
|
||||
};
|
||||
|
||||
auto applySelectedWallpaper = [&]() {
|
||||
if (items.empty() || selIdx < 0 ||
|
||||
selIdx >= static_cast<int>(items.size())) {
|
||||
return;
|
||||
}
|
||||
activePath = items[selIdx].path;
|
||||
applyAndSaveWallpaper(activePath, scaleModeIdx);
|
||||
setBanner("Applied: " + items[selIdx].displayName);
|
||||
};
|
||||
|
||||
if (!isPreload) {
|
||||
showWindow();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
char dch = mapDialKey(sym, kc, ev.xkey.state);
|
||||
if (dch == '*') {
|
||||
fullscreenPreview = !fullscreenPreview;
|
||||
render();
|
||||
continue;
|
||||
} else if (dch == '#') {
|
||||
scaleModeIdx = (scaleModeIdx + 1) % kNumScaleModes;
|
||||
if (!activePath.empty()) {
|
||||
applyAndSaveWallpaper(activePath, scaleModeIdx);
|
||||
}
|
||||
render();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sym == XK_Left || sym == XK_KP_Left) {
|
||||
scaleModeIdx = (scaleModeIdx + kNumScaleModes - 1) % kNumScaleModes;
|
||||
render();
|
||||
} else if (sym == XK_Right || sym == XK_KP_Right) {
|
||||
scaleModeIdx = (scaleModeIdx + 1) % kNumScaleModes;
|
||||
render();
|
||||
} else if (sym == XK_Up || sym == XK_KP_Up) {
|
||||
if (!items.empty()) {
|
||||
selIdx = (selIdx + static_cast<int>(items.size()) - 1) %
|
||||
static_cast<int>(items.size());
|
||||
}
|
||||
render();
|
||||
} else if (sym == XK_Down || sym == XK_KP_Down) {
|
||||
if (!items.empty()) {
|
||||
selIdx = (selIdx + 1) % static_cast<int>(items.size());
|
||||
}
|
||||
render();
|
||||
} else if (sym == XK_Return || sym == XK_KP_Enter) {
|
||||
applySelectedWallpaper();
|
||||
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) {
|
||||
fullscreenPreview = !fullscreenPreview;
|
||||
render();
|
||||
} else if (pend == 2) {
|
||||
if (fullscreenPreview) {
|
||||
fullscreenPreview = false;
|
||||
render();
|
||||
} else {
|
||||
hideWindow();
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
cachedImgPath.clear();
|
||||
if (visible) render();
|
||||
} else if (cmd == "TOGGLE") {
|
||||
if (visible) hideWindow();
|
||||
else showWindow();
|
||||
} else if (cmd.rfind("OPEN", 0) == 0) {
|
||||
showWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideWindow();
|
||||
if (thumbXImg) {
|
||||
thumbXImg->data = nullptr;
|
||||
XDestroyImage(thumbXImg);
|
||||
}
|
||||
if (fullXImg) {
|
||||
fullXImg->data = nullptr;
|
||||
XDestroyImage(fullXImg);
|
||||
}
|
||||
if (fifoFd >= 0) ::close(fifoFd);
|
||||
::unlink(daemonPidPath.c_str());
|
||||
if (xftDraw) XftDrawDestroy(xftDraw);
|
||||
XFreeGC(dpy, gc);
|
||||
XFreePixmap(dpy, pix);
|
||||
XDestroyWindow(dpy, win);
|
||||
XCloseDisplay(dpy);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pluginInit(const DwmHostApi* host) {
|
||||
if (host && host->spawn_plugin) {
|
||||
host->spawn_plugin("wallpapers", "--preload");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pluginCleanup(void) {}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = {
|
||||
DWM_PLUGIN_API_VERSION,
|
||||
"wallpapers",
|
||||
"Bananarchy Desktop Wallpaper Manager (240x320)",
|
||||
pluginInit,
|
||||
pluginCleanup,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
runWallpapersApp
|
||||
};
|
||||
Reference in New Issue
Block a user