946 lines
31 KiB
C++
946 lines
31 KiB
C++
// 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 ringVol = 100;
|
|
int ringTimeSec = 20;
|
|
int ringLoops = 10;
|
|
int vibrateStrength = 2;
|
|
};
|
|
|
|
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" ||
|
|
ext == "aac" || ext == "wma" || ext == "aiff" || ext == "aif" ||
|
|
ext == "mod" || ext == "xm" || ext == "it" || ext == "s3m" ||
|
|
ext == "mptm" || ext == "stm" || ext == "med" || ext == "okt" ||
|
|
ext == "669" || ext == "ult" || ext == "far" || ext == "mtm" ||
|
|
ext == "ptm" || ext == "amf" || ext == "dsm" || ext == "imf" ||
|
|
ext == "gdm" || ext == "stx" || ext == "umx" || ext == "mo3") {
|
|
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);
|
|
scanDirectoryForRingtones("/usr/share/sxmo/music", "Tracker", 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;
|
|
struct stat stDef {};
|
|
if (::stat("/usr/share/sxmo/ringtones/Banana_Chime.wav", &stDef) == 0) {
|
|
return "/usr/share/sxmo/ringtones/Banana_Chime.wav";
|
|
}
|
|
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;
|
|
} else if (k == "SXMO_RINGVOL" && val >= 0 && val <= 100) {
|
|
s.ringVol = (val / 5) * 5;
|
|
}
|
|
}
|
|
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";
|
|
ofs << "SXMO_RINGVOL=" << std::clamp(s.ringVol, 0, 100) << "\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();
|
|
settings = loadRingSettings();
|
|
int rvol = std::clamp(settings.ringVol, 0, 100);
|
|
int rx3 = (rvol <= 0) ? 0 : (60 + rvol * 34 / 100);
|
|
int mpvVol = rvol;
|
|
std::string volArg = "--volume=" + std::to_string(mpvVol);
|
|
runAsyncCmd("amixer -c 0 cset \"name='EAR_S'\" 0 >/dev/null 2>&1; "
|
|
"amixer -c 0 cset \"name='RX1 MIX1 INP1'\" 0 >/dev/null 2>&1; "
|
|
"amixer -c 0 cset \"name='PRI_MI2S_RX Audio Mixer MultiMedia1'\" 1 >/dev/null 2>&1; "
|
|
"amixer -c 0 cset \"name='SPK DAC Switch'\" 1 >/dev/null 2>&1; "
|
|
"amixer -c 0 cset \"name='RX3 MIX1 INP1'\" 3 >/dev/null 2>&1; "
|
|
"amixer -c 0 cset \"name='RX3 Digital Volume'\" " +
|
|
std::to_string(rx3) + " >/dev/null 2>&1; "
|
|
"wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1; "
|
|
"wpctl set-volume @DEFAULT_AUDIO_SINK@ " +
|
|
std::to_string(rvol) + "% >/dev/null 2>&1; "
|
|
"(sleep 0.25; amixer -c 0 cset \"name='RX3 Digital Volume'\" " +
|
|
std::to_string(rx3) + " >/dev/null 2>&1; "
|
|
"wpctl set-volume @DEFAULT_AUDIO_SINK@ " +
|
|
std::to_string(rvol) + "% >/dev/null 2>&1) &");
|
|
pid_t pid = ::fork();
|
|
if (pid == 0) {
|
|
std::string runtimeDir = "/run/user/" + std::to_string(::getuid());
|
|
const char* xdg = std::getenv("XDG_RUNTIME_DIR");
|
|
struct stat stRuntime {};
|
|
if (!xdg || !*xdg || ::stat(xdg, &stRuntime) != 0) {
|
|
::setenv("XDG_RUNTIME_DIR", runtimeDir.c_str(), 1);
|
|
}
|
|
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-config", "--no-terminal",
|
|
"--no-resume-playback", "--quiet", "--vo=null",
|
|
"--no-video", "--ao=pipewire,pulse,alsa",
|
|
"--audio-channels=stereo", "--audio-samplerate=48000",
|
|
"--volume-max=150", volArg.c_str(),
|
|
"--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",
|
|
};
|
|
const char* vibLabels[3] = {"1 (Light)", "2 (Medium)", "3 (Strong)"};
|
|
int vIdx = std::clamp(settings.vibrateStrength, 1, 3) - 1;
|
|
std::string rows[6][2] = {
|
|
{"Ringer Mode",
|
|
"< " + std::string(modeLabels[settings.ringerMode % 4]) + " >"},
|
|
{"Ring Volume",
|
|
"< " + std::to_string(settings.ringVol) + "% >"},
|
|
{"Ring Duration",
|
|
"< " + std::to_string(settings.ringTimeSec) + " sec >"},
|
|
{"Ring Loop Count",
|
|
"< " + std::to_string(settings.ringLoops) + " loops >"},
|
|
{"Vibrate Strength",
|
|
"< Level " + std::string(vibLabels[vIdx]) + " >"},
|
|
{"Test Vibration", "Press OK to vibrate"},
|
|
};
|
|
|
|
for (int i = 0; i < 6; ++i) {
|
|
int ry = 30 + i * 35;
|
|
bool isSel = (settingsRow == i);
|
|
XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgAltEven);
|
|
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 33);
|
|
if (isSel) {
|
|
XSetForeground(dpy, gc, pt.spec.accent);
|
|
XDrawRectangle(dpy, pix, gc, 4, ry, kWinWidth - 9, 32);
|
|
}
|
|
drawPango(layoutSmall, isSel ? &colWhite : &colMuted, 10, ry + 1, 15,
|
|
rows[i][0], kWinWidth - 20);
|
|
drawPango(layoutMain, isSel ? &colYellow : &colBlue, 10, ry + 15, 17,
|
|
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<int>(::getpid()));
|
|
closeOtherPlugins("ringtones");
|
|
refreshData();
|
|
for (size_t i = 0; i < tones.size(); ++i) {
|
|
if (tones[i].path == activePath) {
|
|
selIdx = static_cast<int>(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) {
|
|
settings.ringVol = std::clamp(settings.ringVol + delta * 5, 0, 100);
|
|
saveRingSettings(settings);
|
|
if (previewPid > 0) {
|
|
int rx3 = (settings.ringVol <= 0) ? 0 : (60 + settings.ringVol * 34 / 100);
|
|
runAsyncCmd("amixer -c 0 cset \"name='RX3 Digital Volume'\" " +
|
|
std::to_string(rx3) + " >/dev/null 2>&1; "
|
|
"wpctl set-volume @DEFAULT_AUDIO_SINK@ " +
|
|
std::to_string(settings.ringVol) + "% >/dev/null 2>&1");
|
|
}
|
|
setBanner("Ring volume: " + std::to_string(settings.ringVol) + "%");
|
|
} else if (settingsRow == 2) {
|
|
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 == 3) {
|
|
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 == 4) {
|
|
settings.vibrateStrength =
|
|
((settings.vibrateStrength - 1 + 3 + delta) % 3) + 1;
|
|
saveRingSettings(settings);
|
|
runAsyncCmd("SXMO_VIBRATE_FORCE=1 sxmo_vibrate 450 " +
|
|
std::to_string(settings.vibrateStrength) + " >/dev/null 2>&1");
|
|
setBanner("Vibration level: " + std::to_string(settings.vibrateStrength));
|
|
} else if (settingsRow == 5) {
|
|
runAsyncCmd("SXMO_VIBRATE_FORCE=1 sxmo_vibrate 800 " +
|
|
std::to_string(settings.vibrateStrength) + " >/dev/null 2>&1");
|
|
setBanner("Testing vibration (Level " +
|
|
std::to_string(settings.vibrateStrength) + ")...");
|
|
}
|
|
};
|
|
|
|
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<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 < 5) {
|
|
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 < 5) {
|
|
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 + 5) % 6;
|
|
}
|
|
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) % 6;
|
|
}
|
|
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
|
|
};
|