Add ringtones/wallpapers plugins, AT console, dialer USSD/*#06#, and SIM icon fix

This commit is contained in:
Luxferre
2026-10-24 19:58:11 +03:00
parent e549aaf413
commit dc59704547
64 changed files with 2743 additions and 130 deletions
+288 -19
View File
@@ -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();
+7
View File
@@ -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;
}
+287 -35
View File
@@ -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);
+9 -19
View File
@@ -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");
}
+275 -1
View File
@@ -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
+858
View File
@@ -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
};
+716
View File
@@ -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
};
+15
View File
@@ -2001,6 +2001,10 @@ open_builtin_menu(const char *menu_name, int keep_idx)
}
if (!has_themes)
add_menu_item(label, "@plugin:themes", 0);
snprintf(label, sizeof(label), "%s Wallpapers", ICON_THM);
add_menu_item(label, "@plugin:wallpapers", 0);
snprintf(label, sizeof(label), "%s Ringtones", ICON_BEL);
add_menu_item(label, "@plugin:ringtones", 0);
snprintf(label, sizeof(label), "%s System Menu", ICON_MNU);
add_menu_item(label, "@menu:sys", 1);
snprintf(label, sizeof(label), "%s Close Menu", ICON_CLS);
@@ -2046,6 +2050,8 @@ open_builtin_menu(const char *menu_name, int keep_idx)
add_menu_item(label, "@plugin:modem:info", 0);
snprintf(label, sizeof(label), "%s Send USSD Code", ICON_PHN);
add_menu_item(label, "@plugin:modem:ussd", 0);
snprintf(label, sizeof(label), "%s AT Command Console", ICON_TRM);
add_menu_item(label, "@plugin:modem:at", 0);
snprintf(label, sizeof(label), "%s Modem Event Log", ICON_PHL);
add_menu_item(label, "@plugin:modem:log", 0);
snprintf(label, sizeof(label), "%s System Menu", ICON_MNU);
@@ -2066,6 +2072,9 @@ open_builtin_menu(const char *menu_name, int keep_idx)
snprintf(label, sizeof(label), "%s Volume & Controls (XDrawer)", ICON_MUS);
add_menu_item(label, "@plugin:xdrawer", 0);
snprintf(label, sizeof(label), "%s Ringtones & Ringer", ICON_BEL);
add_menu_item(label, "@plugin:ringtones", 0);
snprintf(pbuf, sizeof(pbuf), "%s/.config/sxmo/.noring", home);
snprintf(label, sizeof(label), "%s Ring %s", ICON_BEL, (access(pbuf, F_OK) == 0) ? ICON_TOF : ICON_TON);
add_menu_item(label, "@toggle:ring", 1);
@@ -2129,6 +2138,10 @@ open_builtin_menu(const char *menu_name, int keep_idx)
snprintf(label, sizeof(label), "%s Themes", ICON_THM);
add_menu_item(label, "@plugin:themes", 0);
snprintf(label, sizeof(label), "%s Wallpapers", ICON_THM);
add_menu_item(label, "@plugin:wallpapers", 0);
snprintf(label, sizeof(label), "%s Ringtones", ICON_BEL);
add_menu_item(label, "@plugin:ringtones", 0);
snprintf(label, sizeof(label), "%s Brightness", ICON_ARU);
add_menu_item(label, "@toggle:bright_up", 1);
snprintf(label, sizeof(label), "%s Brightness", ICON_ARD);
@@ -2295,6 +2308,8 @@ close_other_active_plugins(void)
{ "xthemes.pid", "/tmp/dwm_plugin_themes.fifo" },
{ "xmodem.pid", "/tmp/dwm_plugin_modem.fifo" },
{ "xnetworks.pid", "/tmp/dwm_plugin_networks.fifo" },
{ "xringtones.pid", "/tmp/dwm_plugin_ringtones.fifo" },
{ "xwallpapers.pid", "/tmp/dwm_plugin_wallpapers.fifo" },
};
const char *rdir = getenv("XDG_RUNTIME_DIR");
const char *base = (rdir && rdir[0]) ? rdir : "/run/user/10000";
+2
View File
@@ -784,6 +784,8 @@ static int xdrawer_run_main(int argc, char** argv) {
closeOtherPlugin("xthemes.pid", "/tmp/dwm_plugin_themes.fifo");
closeOtherPlugin("xmodem.pid", "/tmp/dwm_plugin_modem.fifo");
closeOtherPlugin("xnetworks.pid", "/tmp/dwm_plugin_networks.fifo");
closeOtherPlugin("xringtones.pid", "/tmp/dwm_plugin_ringtones.fifo");
closeOtherPlugin("xwallpapers.pid", "/tmp/dwm_plugin_wallpapers.fifo");
theme.checkAndReload(dpy, screen, visual, cmap, layoutMain, layoutSmall,
layoutIcon, nullptr, win);
openStartMs = nowMs();