// 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 #include #include #include #include #include #include #include #include #ifdef None #undef None #endif namespace { using namespace bananarchy; volatile sig_atomic_t gRunning = 1; void handleSignal(int) { gRunning = 0; } enum class TabId { 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(sizeof(kRingTimeSteps) / sizeof(kRingTimeSteps[0])); constexpr int kRingLoopSteps[] = {3, 5, 10, 15, 20, 50}; constexpr int kNumRingLoopSteps = static_cast(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(std::toupper(static_cast(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& out) { DIR* d = ::opendir(dirPath.c_str()); if (!d) return; std::vector 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 discoverRingtones() { std::vector 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(::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 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(tones.size())) { selIdx = std::max(0, static_cast(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(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(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(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 = [&]() { writeIntFile(activePidPath, static_cast(::getpid())); closeOtherPlugins("ringtones"); refreshData(); for (size_t i = 0; i < tones.size(); ++i) { if (tones[i].path == activePath) { selIdx = static_cast(i); break; } } if (!visible) { visible = true; XMapRaised(dpy, win); XSync(dpy, False); } else { XRaiseWindow(dpy, win); } grabPluginKeyboard(dpy, win); startupIgnoreUntilMs = nowMs() + 180; initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); softkeyPending = 0; render(); }; auto hideWindow = [&]() { stopPreview(); if (!visible) return; int xfdLocal = ConnectionNumber(dpy); int64_t drainDeadline = nowMs() + 180; while (nowMs() < drainDeadline && querySoftkeysDown(dpy) != 0) { while (XPending(dpy) > 0) { XEvent ev; XNextEvent(dpy, &ev); } struct pollfd pfd {}; pfd.fd = xfdLocal; pfd.events = POLLIN; ::poll(&pfd, 1, 10); } writeSoftkeySuppress(nowMs() + 450); XUngrabKeyboard(dpy, CurrentTime); 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) { int mag = (settings.vibrateStrength == 1) ? 20000 : (settings.vibrateStrength == 3 ? 65535 : 40000); runAsyncCmd( "for d in /dev/input/by-path/platform-vibrator-event " "/dev/input/by-path/*vibrator*-event; do " "[ -e \"$d\" ] && export SXMO_VIBRATE_DEV=\"$d\" && break; done; " "sxmo_vibrate 600 " + std::to_string(mag) + " >/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) { if (isAnotherPluginActive("ringtones")) { hideWindow(); } else { XRaiseWindow(dpy, win); grabPluginKeyboard(dpy, win); render(); } } else if (ev.type == KeyRelease) { if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { initialSoftkeysReleased = true; } } else if (ev.type == KeyPress && visible) { int64_t now = nowMs(); if (now < startupIgnoreUntilMs) continue; KeySym sym = XLookupKeysym(&ev.xkey, 0); unsigned int kc = ev.xkey.keycode; bool isLSK = (kc == 135 || sym == XK_F1 || sym == XK_Menu || sym == XF86XK_TaskPane); bool isRSK = (kc == 166 || sym == XK_F2 || sym == XF86XK_Back || sym == XK_Escape || sym == static_cast(XF86XK_KbdLcdMenu2)); if (isLSK || isRSK) { if (!initialSoftkeysReleased) continue; int mask = querySoftkeysDown(dpy); if (mask == 3 || (softkeyPending != 0 && ((isLSK && softkeyPending == 2) || (isRSK && softkeyPending == 1)))) { softkeyPending = 0; writeSoftkeySuppress(now + 400); hideWindow(); sendPluginCommand("xdrawer", "TOGGLE"); continue; } softkeyPending = isLSK ? 1 : 2; softkeyDeadlineMs = now + 90; continue; } if (sym == XF86XK_HangupPhone || sym == XK_Cancel) { hideWindow(); continue; } 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(tones.size()) - 1) % static_cast(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(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(rem); } } if (!statusBanner.empty()) { int64_t bRem = statusBannerUntilMs - now; if (bRem <= 0) { statusBanner.clear(); render(); } else if (timeoutMs < 0 || bRem < timeoutMs) { timeoutMs = static_cast(bRem); } } 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 };