Add xmusic tracker/audio player, xfiles media actions, ringtone volume & vibration fix

This commit is contained in:
Luxferre
2026-10-25 17:31:54 +02:00
parent ba2307d44c
commit 30c3e817dc
70 changed files with 2296 additions and 86 deletions
+4
View File
@@ -281,6 +281,10 @@ static const char *resolve_plugin_name_from_argv0(const char *argv0) {
strcasecmp(base, "xbackground") == 0 || strcasecmp(base, "background") == 0) {
return "wallpapers";
}
if (strcasecmp(base, "xmusic") == 0 || strcasecmp(base, "music") == 0 ||
strcasecmp(base, "xplayer") == 0) {
return "music";
}
return NULL;
}
+177 -20
View File
@@ -93,12 +93,28 @@ std::string getExtLower(const std::string& name) {
}
bool isImageExt(const std::string& ext) {
return ext == "jpg" || ext == "jpeg" || ext == "png";
return ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "bmp" ||
ext == "gif" || ext == "webp" || ext == "ppm" || ext == "pgm" ||
ext == "pbm" || ext == "tif" || ext == "tiff" || ext == "ico" ||
ext == "svg" || ext == "avif" || ext == "heic";
}
bool isAudioExt(const std::string& ext) {
return ext == "mp3" || ext == "wav" || ext == "ogg" || ext == "oga" ||
ext == "opus" || ext == "flac" || ext == "m4a" || ext == "aac" ||
ext == "wma" || ext == "aiff" || ext == "wv" || ext == "ape" ||
ext == "spx" || 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" || ext == "j2b" || ext == "psm" || ext == "dbm" ||
ext == "digi" || ext == "mdl" || ext == "mt2" || ext == "ams" ||
ext == "plm" || ext == "sfx";
}
bool isMediaExt(const std::string& ext) {
return ext == "mp3" || ext == "wav" || ext == "ogg" || ext == "flac" ||
ext == "m4a" || ext == "mp4" || ext == "mkv" || ext == "webm" ||
return isAudioExt(ext) || ext == "mp4" || ext == "mkv" || ext == "webm" ||
ext == "avi";
}
@@ -311,9 +327,17 @@ int runFilesApp(int argc, char** argv) {
dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0,
reinterpret_cast<char*>(imgCanvas.data()), kImgCanvasW, kImgCanvasH, 32,
kImgCanvasW * 4);
constexpr int kImgFullW = 240;
constexpr int kImgFullH = 270;
std::vector<uint32_t> imgFullCanvas(kImgFullW * kImgFullH, 0x111111);
XImage* xImgFullPreview = XCreateImage(
dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0,
reinterpret_cast<char*>(imgFullCanvas.data()), kImgFullW, kImgFullH, 32,
kImgFullW * 4);
int imgOrigW = 0;
int imgOrigH = 0;
bool imgValid = false;
bool imgFullscreen = false;
// Modal input state (New Folder / New File / Rename)
InputAction inputAction = InputAction::NewFolder;
@@ -487,19 +511,113 @@ int runFilesApp(int argc, char** argv) {
if (imgValid) {
scaleRgb24ToBgrxCanvas(rgb.data(), imgOrigW, imgOrigH, imgCanvas.data(),
kImgCanvasW, kImgCanvasH, 0x111111);
scaleRgb24ToBgrxCanvas(rgb.data(), imgOrigW, imgOrigH,
imgFullCanvas.data(), kImgFullW, kImgFullH,
0x111111);
} else {
std::fill(imgCanvas.begin(), imgCanvas.end(), 0x111111);
std::fill(imgFullCanvas.begin(), imgFullCanvas.end(), 0x111111);
}
mode = ViewMode::ImageView;
};
auto installAsWallpaper = [&](const std::string& srcPath,
const std::string& fileName) {
std::string targetPath = srcPath;
std::string wallDir = homeDir + "/Pictures/Wallpapers";
std::string picDir = homeDir + "/Pictures";
if (srcPath.rfind("/usr/share/wallpapers/", 0) != 0 &&
srcPath.rfind(wallDir + "/", 0) != 0 &&
srcPath.rfind(picDir + "/", 0) != 0) {
::mkdir(picDir.c_str(), 0755);
::mkdir(wallDir.c_str(), 0755);
std::string dst = wallDir + "/" + fileName;
if (copyRecursive(srcPath, dst)) {
targetPath = dst;
}
}
std::string cfgDir = homeDir + "/.config/sxmo";
const char* xdgCfg = std::getenv("XDG_CONFIG_HOME");
if (xdgCfg && *xdgCfg) cfgDir = std::string(xdgCfg) + "/sxmo";
::mkdir((homeDir + "/.config").c_str(), 0755);
::mkdir(cfgDir.c_str(), 0755);
{
std::ofstream ofs(cfgDir + "/wallpaper", std::ios::trunc);
if (ofs) ofs << targetPath << "\n";
}
std::string fehFlag = "--bg-fill";
{
std::ifstream ifs(cfgDir + "/wallpaper_mode");
std::string m;
if (ifs && std::getline(ifs, m)) {
m = toLowerStr(trimStr(m));
if (m == "max" || m == "--bg-max") fehFlag = "--bg-max";
else if (m == "scale" || m == "--bg-scale") fehFlag = "--bg-scale";
else if (m == "center" || m == "--bg-center") fehFlag = "--bg-center";
else if (m == "tile" || m == "--bg-tile") fehFlag = "--bg-tile";
}
}
runAsyncCmd("feh --no-fehbg " + fehFlag + " " + shellQuote(targetPath) +
" >/dev/null 2>&1; "
"PATH=\"$HOME/.config/sxmo/hooks:/usr/share/sxmo/default_hooks:$PATH\" "
"sxmo_hook_statusbar.sh time >/dev/null 2>&1");
statusBanner = "Wallpaper set: " + fileName;
statusBannerUntilMs = nowMs() + 2200;
};
auto installAsRingtone = [&](const std::string& srcPath,
const std::string& fileName) {
std::string targetPath = srcPath;
std::string ringDir = homeDir + "/Ringtones";
std::string musDir = homeDir + "/Music";
if (srcPath.rfind("/usr/share/sxmo/", 0) != 0 &&
srcPath.rfind(ringDir + "/", 0) != 0 &&
srcPath.rfind(musDir + "/", 0) != 0) {
::mkdir(ringDir.c_str(), 0755);
std::string dst = ringDir + "/" + fileName;
if (copyRecursive(srcPath, dst)) {
targetPath = dst;
}
}
std::string cfgDir = homeDir + "/.config/sxmo";
const char* xdgCfg = std::getenv("XDG_CONFIG_HOME");
if (xdgCfg && *xdgCfg) cfgDir = std::string(xdgCfg) + "/sxmo";
::mkdir((homeDir + "/.config").c_str(), 0755);
::mkdir(cfgDir.c_str(), 0755);
{
std::ofstream ofs(cfgDir + "/ringtone", std::ios::trunc);
if (ofs) ofs << targetPath << "\n";
}
statusBanner = "Ringtone set: " + fileName;
statusBannerUntilMs = nowMs() + 2200;
};
auto buildActionsList = [&]() {
actionsList.clear();
bool hasSel = !filteredItems.empty();
bool isParent = hasSel && filteredItems[selected].isParent;
if (hasSel) {
actionsList.push_back({1, kIconEye, "Open / View"});
if (!filteredItems[selected].isDir) {
std::string ext = getExtLower(filteredItems[selected].name);
if (isImageExt(ext)) {
actionsList.push_back({1, kIconEye, "Preview Image"});
actionsList.push_back({14, kIconImage, "Set as Wallpaper"});
} else if (isAudioExt(ext)) {
actionsList.push_back({1, kIconMusic, "Play in Music Player"});
actionsList.push_back({15, kIconBell, "Set as Ringtone"});
} else {
actionsList.push_back({1, kIconEye, "Open / View"});
}
} else {
actionsList.push_back({1, kIconFolderOpen, "Open Folder"});
}
}
if (!clipPath.empty()) {
actionsList.push_back(
@@ -540,6 +658,15 @@ int runFilesApp(int argc, char** argv) {
::unlink(activePidPath.c_str());
};
auto openInMusicPlayer = [&](const std::string& path) {
hideWindow();
if (isPluginDaemonAlive("music")) {
sendPluginCommand("music", "OPEN " + path);
} else {
runAsyncArgv("xmusic", path.c_str());
}
};
auto openSelectedItem = [&]() {
if (filteredItems.empty()) return;
const FileItem item = filteredItems[selected];
@@ -557,7 +684,11 @@ int runFilesApp(int argc, char** argv) {
}
std::string ext = getExtLower(item.name);
if (isImageExt(ext)) {
imgFullscreen = false;
openImageViewer(item);
} else if (isAudioExt(ext)) {
openInMusicPlayer(item.fullPath);
return;
} else if (isMediaExt(ext)) {
hideWindow();
runAsyncArgv("sxmo_open.sh", item.fullPath.c_str());
@@ -786,19 +917,25 @@ int runFilesApp(int argc, char** argv) {
drawSoftkeyBar("Edit", "Down", "Back");
} else if (mode == ViewMode::ImageView) {
XSetForeground(dpy, gc, pt.spec.bgHeader);
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36);
drawPango(layoutIcon, &colGreen, 6, 1, 18, kIconImage);
drawPango(layoutMain, &colWhite, 28, 0, 20, viewingFileName, 145);
std::string dim = imgValid
? (std::to_string(imgOrigW) + "x" +
std::to_string(imgOrigH))
: "Decode failed";
drawPango(layoutSmall, &colMuted, 28, 19, 15, dim, 200);
if (imgFullscreen) {
XPutImage(dpy, pix, gc, xImgFullPreview, 0, 0, 0, 0, kImgFullW,
kImgFullH);
} else {
XSetForeground(dpy, gc, pt.spec.bgHeader);
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36);
drawPango(layoutIcon, &colGreen, 6, 1, 18, kIconImage);
drawPango(layoutMain, &colWhite, 28, 0, 20, viewingFileName, 145);
std::string dim = imgValid
? (std::to_string(imgOrigW) + "x" +
std::to_string(imgOrigH) + " ([*] Full)")
: "Decode failed";
drawPango(layoutSmall, &colMuted, 28, 19, 15, dim, 200);
XPutImage(dpy, pix, gc, xImgPreview, 0, 0, 0, 36, kImgCanvasW,
kImgCanvasH);
drawSoftkeyBar("Full", "Next", "Back");
XPutImage(dpy, pix, gc, xImgPreview, 0, 0, 0, 36, kImgCanvasW,
kImgCanvasH);
}
drawSoftkeyBar("Set Wall", "Next",
imgFullscreen ? "Header" : "Back");
} else if (mode == ViewMode::PropsView && !filteredItems.empty()) {
const auto& it = filteredItems[selected];
@@ -1027,6 +1164,14 @@ int runFilesApp(int argc, char** argv) {
} else if (actId == 13 && !filteredItems.empty()) {
confirmSelected = 0;
mode = ViewMode::ConfirmDelete;
} else if (actId == 14 && !filteredItems.empty()) {
installAsWallpaper(filteredItems[selected].fullPath,
filteredItems[selected].name);
mode = ViewMode::Browser;
} else if (actId == 15 && !filteredItems.empty()) {
installAsRingtone(filteredItems[selected].fullPath,
filteredItems[selected].name);
mode = ViewMode::Browser;
}
};
@@ -1077,9 +1222,13 @@ int runFilesApp(int argc, char** argv) {
}
} else if (mode == ViewMode::ImageView) {
if (isLeft) {
runAsyncArgv("feh", viewingFilePath.c_str());
installAsWallpaper(viewingFilePath, viewingFileName);
} else {
mode = ViewMode::Browser;
if (imgFullscreen) {
imgFullscreen = false;
} else {
mode = ViewMode::Browser;
}
}
} else if (mode == ViewMode::PropsView) {
mode = ViewMode::Browser;
@@ -1300,8 +1449,12 @@ int runFilesApp(int argc, char** argv) {
drawUI();
}
} else if (mode == ViewMode::ImageView) {
if (sym == XK_Left || sym == XK_KP_Left || sym == XK_Up ||
sym == XK_KP_Up) {
char d = mapDialKey(sym, kc, ev.xkey.state);
if (d == '*') {
imgFullscreen = !imgFullscreen;
drawUI();
} else if (sym == XK_Left || sym == XK_KP_Left || sym == XK_Up ||
sym == XK_KP_Up) {
stepImageInFolder(-1);
drawUI();
} else if (sym == XK_Right || sym == XK_KP_Right || sym == XK_Down ||
@@ -1395,6 +1548,10 @@ int runFilesApp(int argc, char** argv) {
xImgPreview->data = nullptr;
XDestroyImage(xImgPreview);
}
if (xImgFullPreview) {
xImgFullPreview->data = nullptr;
XDestroyImage(xImgFullPreview);
}
if (fifoFd >= 0) ::close(fifoFd);
::unlink(daemonPidPath.c_str());
return 0;
+72
View File
@@ -14,6 +14,8 @@
#include <jpeglib.h>
#include <png.h>
#include <sys/wait.h>
#include <unistd.h>
namespace bananarchy {
@@ -159,6 +161,73 @@ inline bool loadPngRgb24(const std::string& path,
return true;
}
inline bool loadFfmpegImageRgb24(const std::string& path,
std::vector<uint8_t>* outRgb,
int* outW,
int* outH) {
int pipefd[2];
if (::pipe(pipefd) != 0) {
return false;
}
pid_t pid = ::fork();
if (pid < 0) {
::close(pipefd[0]);
::close(pipefd[1]);
return false;
}
if (pid == 0) {
::close(pipefd[0]);
::dup2(pipefd[1], STDOUT_FILENO);
if (pipefd[1] != STDOUT_FILENO) ::close(pipefd[1]);
int devnull = ::open("/dev/null", O_RDWR);
if (devnull >= 0) {
::dup2(devnull, STDIN_FILENO);
::dup2(devnull, STDERR_FILENO);
if (devnull > 2) ::close(devnull);
}
::execlp("ffmpeg", "ffmpeg", "-v", "error", "-i", path.c_str(),
"-frames:v", "1", "-f", "image2pipe", "-vcodec", "png", "-",
static_cast<char*>(nullptr));
::_exit(127);
}
::close(pipefd[1]);
std::vector<uint8_t> pngBuf;
uint8_t chunk[4096];
ssize_t n = 0;
while ((n = ::read(pipefd[0], chunk, sizeof(chunk))) > 0) {
if (pngBuf.size() + static_cast<size_t>(n) > 16 * 1024 * 1024) break;
pngBuf.insert(pngBuf.end(), chunk, chunk + n);
}
::close(pipefd[0]);
int status = 0;
::waitpid(pid, &status, 0);
if (pngBuf.empty()) {
return false;
}
png_image image {};
image.version = PNG_IMAGE_VERSION;
if (!png_image_begin_read_from_memory(&image, pngBuf.data(), pngBuf.size())) {
return false;
}
image.format = PNG_FORMAT_RGB;
int w = static_cast<int>(image.width);
int h = static_cast<int>(image.height);
if (w <= 0 || h <= 0 || w > 8192 || h > 8192) {
png_image_free(&image);
return false;
}
outRgb->resize(PNG_IMAGE_SIZE(image));
if (!png_image_finish_read(&image, nullptr, outRgb->data(), 0, nullptr)) {
png_image_free(&image);
return false;
}
png_image_free(&image);
*outW = w;
*outH = h;
return true;
}
inline bool loadImageRgb24(const std::string& path,
std::vector<uint8_t>* outRgb,
int* outW,
@@ -178,6 +247,9 @@ inline bool loadImageRgb24(const std::string& path,
if (loadPngRgb24(path, outRgb, outW, outH)) {
return true;
}
if (loadFfmpegImageRgb24(path, outRgb, outW, outH)) {
return true;
}
return false;
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -1156,6 +1156,7 @@ inline void closeOtherPlugins(const char* selfPluginName) {
{"xnetworks.pid", "networks"},
{"xringtones.pid", "ringtones"},
{"xwallpapers.pid", "wallpapers"},
{"xmusic.pid", "music"},
};
for (const auto& p : kAllPlugins) {
if (selfPluginName && std::strcmp(p.name, selfPluginName) == 0) {
@@ -1190,6 +1191,7 @@ inline bool isAnotherPluginActive(const char* selfPluginName) {
{"xnetworks.pid", "networks"},
{"xringtones.pid", "ringtones"},
{"xwallpapers.pid", "wallpapers"},
{"xmusic.pid", "music"},
};
for (const auto& p : kAllPlugins) {
if (selfPluginName && std::strcmp(p.name, selfPluginName) == 0) {
@@ -1233,6 +1235,7 @@ inline void broadcastThemeReload(const char* selfPluginName = nullptr) {
"networks",
"ringtones",
"wallpapers",
"music",
};
for (const char* name : kPlugins) {
if (selfPluginName && std::strcmp(name, selfPluginName) == 0) {
+38 -17
View File
@@ -43,6 +43,7 @@ struct RingtoneEntry {
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 = 1;
@@ -198,6 +199,8 @@ RingSettings loadRingSettings() {
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;
@@ -228,6 +231,7 @@ void saveRingSettings(const RingSettings& s) {
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");
}
@@ -372,6 +376,16 @@ int runRingtonesApp(int argc, char** argv) {
return;
}
stopPreview();
settings = loadRingSettings();
int rvol = std::clamp(settings.ringVol, 0, 100);
int rx3 = (rvol <= 0) ? 0 : (60 + rvol * 34 / 100);
int mpvVol = rvol * 130 / 100;
std::string volArg = "--volume=" + std::to_string(mpvVol);
runAsyncCmd("amixer -c 0 cset \"name='SPK DAC Switch'\" 1 >/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@ 100% >/dev/null 2>&1");
pid_t pid = ::fork();
if (pid == 0) {
int devNull = ::open("/dev/null", O_RDWR);
@@ -382,7 +396,8 @@ int runRingtonesApp(int argc, char** argv) {
if (devNull > 2) ::close(devNull);
}
::execlp("mpv", "mpv", "--no-resume-playback", "--quiet",
"--no-video", "--loop=2", path.c_str(), nullptr);
"--no-video", "--volume-max=150", volArg.c_str(),
"--loop=2", path.c_str(), nullptr);
::_exit(127);
} else if (pid > 0) {
previewPid = pid;
@@ -532,9 +547,11 @@ int runRingtonesApp(int argc, char** argv) {
"Vibrate Only",
"Silent",
};
std::string rows[5][2] = {
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",
@@ -544,18 +561,18 @@ int runRingtonesApp(int argc, char** argv) {
{"Test Vibration", "Press OK to vibrate"},
};
for (int i = 0; i < 5; ++i) {
int ry = 32 + i * 42;
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, 38);
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, 37);
XDrawRectangle(dpy, pix, gc, 4, ry, kWinWidth - 9, 32);
}
drawPango(layoutSmall, isSel ? &colWhite : &colMuted, 10, ry + 2, 17,
drawPango(layoutSmall, isSel ? &colWhite : &colMuted, 10, ry + 1, 15,
rows[i][0], kWinWidth - 20);
drawPango(layoutMain, isSel ? &colYellow : &colBlue, 10, ry + 18, 19,
drawPango(layoutMain, isSel ? &colYellow : &colBlue, 10, ry + 15, 17,
rows[i][1], kWinWidth - 20);
}
drawSoftkeyBar("Change", "Select", "Close");
@@ -629,6 +646,10 @@ int runRingtonesApp(int argc, char** argv) {
saveRingSettings(settings);
setBanner("Ringer mode updated");
} else if (settingsRow == 1) {
settings.ringVol = std::clamp(settings.ringVol + delta * 5, 0, 100);
saveRingSettings(settings);
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) {
@@ -639,7 +660,7 @@ int runRingtonesApp(int argc, char** argv) {
curIdx = (curIdx + kNumRingTimeSteps + delta) % kNumRingTimeSteps;
settings.ringTimeSec = kRingTimeSteps[curIdx];
saveRingSettings(settings);
} else if (settingsRow == 2) {
} else if (settingsRow == 3) {
int curIdx = 2;
for (int i = 0; i < kNumRingLoopSteps; ++i) {
if (kRingLoopSteps[i] == settings.ringLoops) {
@@ -650,14 +671,14 @@ int runRingtonesApp(int argc, char** argv) {
curIdx = (curIdx + kNumRingLoopSteps + delta) % kNumRingLoopSteps;
settings.ringLoops = kRingLoopSteps[curIdx];
saveRingSettings(settings);
} else if (settingsRow == 3) {
} else if (settingsRow == 4) {
settings.vibrateStrength =
((settings.vibrateStrength - 1 + 3 + delta) % 3) + 1;
saveRingSettings(settings);
} else if (settingsRow == 4) {
} else if (settingsRow == 5) {
int mag = (settings.vibrateStrength == 1)
? 20000
: (settings.vibrateStrength == 3 ? 65535 : 40000);
? 8000
: (settings.vibrateStrength == 3 ? 65535 : 32000);
runAsyncCmd(
"for d in /dev/input/by-path/platform-vibrator-event "
"/dev/input/by-path/*vibrator*-event; do "
@@ -744,7 +765,7 @@ int runRingtonesApp(int argc, char** argv) {
}
if (sym == XK_Left || sym == XK_KP_Left) {
if (activeTab == TabId::Settings && settingsRow < 4) {
if (activeTab == TabId::Settings && settingsRow < 5) {
adjustSettingRow(-1);
} else {
activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings
@@ -752,7 +773,7 @@ int runRingtonesApp(int argc, char** argv) {
}
render();
} else if (sym == XK_Right || sym == XK_KP_Right) {
if (activeTab == TabId::Settings && settingsRow < 4) {
if (activeTab == TabId::Settings && settingsRow < 5) {
adjustSettingRow(1);
} else {
activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings
@@ -764,14 +785,14 @@ int runRingtonesApp(int argc, char** argv) {
selIdx = (selIdx + static_cast<int>(tones.size()) - 1) %
static_cast<int>(tones.size());
} else if (activeTab == TabId::Settings) {
settingsRow = (settingsRow + 4) % 5;
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) % 5;
settingsRow = (settingsRow + 1) % 6;
}
render();
} else if (sym == XK_Return || sym == XK_KP_Enter) {
+150
View File
@@ -0,0 +1,150 @@
#include <errno.h>
#include <fcntl.h>
#include <glob.h>
#include <linux/input.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>
static int read_configured_level(void) {
char path[512];
const char* cfg = getenv("XDG_CONFIG_HOME");
const char* home = getenv("HOME");
if (cfg && *cfg) {
snprintf(path, sizeof(path), "%s/sxmo/ringsettings", cfg);
} else if (home && *home) {
snprintf(path, sizeof(path), "%s/.config/sxmo/ringsettings", home);
} else {
snprintf(path, sizeof(path), "/home/user/.config/sxmo/ringsettings");
}
FILE* fp = fopen(path, "r");
if (!fp) return 0;
int level = 0;
char line[256];
while (fgets(line, sizeof(line), fp)) {
char* p = line;
while (*p == ' ' || *p == '\t') ++p;
if (strncmp(p, "SXMO_VIBRATE_STRENGTH=", 22) == 0) {
int v = atoi(p + 22);
if (v >= 1 && v <= 3) level = v;
}
}
fclose(fp);
return level;
}
static int resolve_magnitude(int raw_arg, int has_arg) {
int val = raw_arg;
if (!has_arg || (val == 1 && !getenv("SXMO_VIBRATE_STRENGTH"))) {
int cfg = read_configured_level();
if (cfg >= 1 && cfg <= 3) val = cfg;
}
if (val <= 1) return 8000;
if (val == 2) return 32000;
if (val == 3) return 65535;
if (val < 256) return val * 257;
if (val > 65535) return 65535;
return val;
}
static int open_vibrator_device(void) {
const char* env_dev = getenv("SXMO_VIBRATE_DEV");
if (env_dev && *env_dev) {
int fd = open(env_dev, O_RDWR | O_CLOEXEC);
if (fd >= 0) return fd;
}
int fd = open("/dev/input/by-path/platform-vibrator-event", O_RDWR | O_CLOEXEC);
if (fd >= 0) return fd;
glob_t g;
memset(&g, 0, sizeof(g));
if (glob("/dev/input/by-path/*vibrator*-event", 0, NULL, &g) == 0) {
for (size_t i = 0; i < g.gl_pathc; ++i) {
fd = open(g.gl_pathv[i], O_RDWR | O_CLOEXEC);
if (fd >= 0) {
globfree(&g);
return fd;
}
}
globfree(&g);
}
return -1;
}
int main(int argc, char** argv) {
if (argc < 2 || argc > 3) {
fprintf(stderr, "Usage: sxmo_vibrate duration_ms [strength_number]\n");
return 1;
}
char* endp = NULL;
errno = 0;
long duration_ms = strtol(argv[1], &endp, 10);
if (errno != 0 || !endp || *endp != '\0' || duration_ms <= 0) {
fprintf(stderr, "sxmo_vibrate: invalid duration\n");
return 1;
}
int raw_strength = 2;
int has_strength = 0;
if (argc == 3) {
endp = NULL;
errno = 0;
long s = strtol(argv[2], &endp, 10);
if (errno == 0 && endp && *endp == '\0' && s >= 1 && s <= 65535) {
raw_strength = (int)s;
has_strength = 1;
}
}
int magnitude = resolve_magnitude(raw_strength, has_strength);
int fd = open_vibrator_device();
if (fd < 0) {
fprintf(stderr, "sxmo_vibrate: cannot open vibrator device: %s\n",
strerror(errno));
return 1;
}
struct ff_effect effect;
memset(&effect, 0, sizeof(effect));
effect.type = FF_RUMBLE;
effect.id = -1;
effect.u.rumble.strong_magnitude = (unsigned short)magnitude;
effect.u.rumble.weak_magnitude = (unsigned short)magnitude;
if (ioctl(fd, EVIOCSFF, &effect) < 0) {
close(fd);
return 1;
}
struct input_event play;
memset(&play, 0, sizeof(play));
play.type = EV_FF;
play.code = (unsigned short)effect.id;
play.value = 3;
if (write(fd, &play, sizeof(play)) < 0) {
ioctl(fd, EVIOCRMFF, effect.id);
close(fd);
return 1;
}
struct timespec req;
req.tv_sec = duration_ms / 1000;
req.tv_nsec = (duration_ms % 1000) * 1000000L;
while (nanosleep(&req, &req) == -1 && errno == EINTR) {
}
play.value = 0;
(void)write(fd, &play, sizeof(play));
(void)ioctl(fd, EVIOCRMFF, effect.id);
close(fd);
return 0;
}
+8
View File
@@ -2275,6 +2275,9 @@ open_builtin_menu(const char *menu_name, int keep_idx)
snprintf(label, sizeof(label), "%s Camera", ICON_CAM);
add_menu_item(label, "@plugin:camera", 0);
snprintf(label, sizeof(label), "%s Music", ICON_MUS);
add_menu_item(label, "@plugin:music", 0);
snprintf(label, sizeof(label), "%s Themes", ICON_THM);
add_menu_item(label, "@plugin:themes", 0);
@@ -2337,6 +2340,8 @@ open_builtin_menu(const char *menu_name, int keep_idx)
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 Music", ICON_MUS);
add_menu_item(label, "@plugin:music", 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);
@@ -2401,6 +2406,9 @@ open_builtin_menu(const char *menu_name, int keep_idx)
rdir = "/run/user/10000";
char pbuf[256];
snprintf(label, sizeof(label), "%s Music Player", ICON_MUS);
add_menu_item(label, "@plugin:music", 0);
snprintf(label, sizeof(label), "%s Volume & Controls (XDrawer)", ICON_MUS);
add_menu_item(label, "@plugin:xdrawer", 0);
+107 -28
View File
@@ -26,6 +26,7 @@
#include <cstring>
#include <ctime>
#include <fcntl.h>
#include <fstream>
#include <poll.h>
#include <sstream>
#include <string>
@@ -42,7 +43,7 @@ constexpr int kWinX = 0;
constexpr int kWinY = kTopBarHeight;
constexpr int kWinWidth = kScreenWidth;
constexpr int kWinHeight = kScreenHeight - kTopBarHeight; // 296px flush below 24px dwm bar
constexpr int kRowCount = 8;
constexpr int kRowCount = 9;
// Nerd Font PUA icons matching sxmo_hook_icons.sh & dwm ("Fira Code, DejaVuSansM Nerd Font")
constexpr const char* kIconSpeaker = "\xef\x80\xa8"; // U+F028 speaker
@@ -239,12 +240,13 @@ class AlsaControl {
enum class ControlId {
SpeakerVol = 0,
EarpieceVol = 1,
Flashlight = 2,
SoundMute = 3,
MicMute = 4,
RingerMode = 5,
ScreenBright = 6,
KbdBright = 7,
RingVol = 2,
RingerMode = 3,
Flashlight = 4,
SoundMute = 5,
MicMute = 6,
ScreenBright = 7,
KbdBright = 8,
};
struct ControlItem {
@@ -282,31 +284,55 @@ class PhoneController {
void refreshAll() {
items_.clear();
// 1. Speaker volume (0..100%, mapped to ALSA RX3 Digital Volume 0..84)
long rx3 = alsa_.getInteger("RX3 Digital Volume", 84);
int spkPct = std::clamp(static_cast<int>(std::lround((rx3 * 100.0) / 84.0)), 0, 100);
// 1. Speaker volume (0..100%, mapped to ALSA RX3 Digital Volume 60..92 + PipeWire 0..100%)
int spkPct = readIntFile(getSxmoCfgDir() + "/speaker_vol", -1);
if (spkPct < 0 || spkPct > 100) {
long rx3 = alsa_.getInteger("RX3 Digital Volume", 92);
if (rx3 <= 0) {
spkPct = 0;
} else if (rx3 >= 84) {
spkPct = 100;
} else {
spkPct = std::clamp(static_cast<int>(std::lround(((rx3 - 60) * 100.0) / 32.0)), 5, 100);
}
}
spkPct = (spkPct / 5) * 5;
items_.push_back({ControlId::SpeakerVol, kIconSpeaker, kIconSoundOff,
"Speaker", true, 0, 100, 5, spkPct});
// 2. Earpiece volume (0..100%, mapped to ALSA RX1 Digital Volume 0..84)
// 2. Earpiece volume (0..100%, mapped to ALSA RX1 Digital Volume 58..90)
long rx1 = alsa_.getInteger("RX1 Digital Volume", 84);
int earPct = std::clamp(static_cast<int>(std::lround((rx1 * 100.0) / 84.0)), 0, 100);
int earPct = 100;
if (rx1 <= 0) {
earPct = 0;
} else if (rx1 < 84) {
earPct = std::clamp(static_cast<int>(std::lround((rx1 * 100.0) / 84.0)), 5, 100);
}
earPct = (earPct / 5) * 5;
items_.push_back({ControlId::EarpieceVol, kIconEarpiece, kIconEarpiece,
"Earpiece", true, 0, 100, 5, earPct});
// 3. Flashlight on/off (toggle)
// 3. Ringtone volume (0..100%, stored as SXMO_RINGVOL in ~/.config/sxmo/ringsettings)
int ringPct = readRingVolume();
items_.push_back({ControlId::RingVol, kIconBell, kIconBellSlash,
"Ring Vol", true, 0, 100, 5, ringPct});
// 4. Ringer Mode (0=Ring+Vib, 1=Vibrate, 2=Silent, 3=DND)
int ringerVal = readRingerMode();
items_.push_back({ControlId::RingerMode, kIconBell, kIconBellSlash,
"Ringer", false, 0, 3, 1, ringerVal});
// 5. Flashlight on/off (toggle)
int torchCur = readIntFile(torchDir_ + "/brightness", 0);
items_.push_back({ControlId::Flashlight, kIconFlashlight, kIconFlashlight,
"Flashlight", false, 0, 1, 1, (torchCur > 0) ? 1 : 0});
// 4. Sound on/off (toggle)
// 6. Sound on/off (toggle)
bool spkOn = alsa_.getBoolean("SPK DAC Switch", true);
items_.push_back({ControlId::SoundMute, kIconSoundOn, kIconSoundOff,
"Sound", false, 0, 1, 1, spkOn ? 1 : 0});
// 5. Microphone on/off (toggle)
// 7. Microphone on/off (toggle)
unsigned int dec1 = alsa_.getEnum("DEC1 MUX", 2);
if (dec1 > 0) {
savedMicMux_ = dec1;
@@ -317,18 +343,13 @@ class PhoneController {
items_.push_back({ControlId::MicMute, kIconMicOn, kIconMicOff,
"Microphone", false, 0, 1, 1, (dec1 > 0) ? 1 : 0});
// 6. Ringer Mode (0=Ring+Vib, 1=Vibrate, 2=Silent, 3=DND)
int ringerVal = readRingerMode();
items_.push_back({ControlId::RingerMode, kIconBell, kIconBellSlash,
"Ringer", false, 0, 3, 1, ringerVal});
// 7. Screen brightness (slider 1..screenMax_)
// 8. Screen brightness (slider 1..screenMax_)
int scrCur = std::clamp(readIntFile(backlightDir_ + "/brightness", screenMax_), 1, screenMax_);
int scrStep = std::max(1, screenMax_ / 14);
items_.push_back({ControlId::ScreenBright, kIconBrightness, kIconBrightness,
"Screen", true, 1, screenMax_, scrStep, scrCur});
// 7. Keyboard brightness (slider if max_brightness > 1, toggle otherwise)
// 9. Keyboard brightness (slider if max_brightness > 1, toggle otherwise)
int kbdCur = std::clamp(readIntFile(kbdDir_ + "/brightness", 0), 0, kbdMax_);
if (kbdSupportsSlider_) {
int kbdStep = std::max(1, kbdMax_ / 10);
@@ -395,25 +416,81 @@ class PhoneController {
return 0; // Ring + Vibrate
}
static int readRingVolume() {
std::ifstream ifs(getSxmoCfgDir() + "/ringsettings");
std::string line;
while (std::getline(ifs, line)) {
auto eq = line.find('=');
if (eq == std::string::npos) continue;
std::string k = line.substr(0, eq);
std::string v = line.substr(eq + 1);
if (k == "SXMO_RINGVOL") {
int val = std::clamp(std::atoi(v.c_str()), 0, 100);
return (val / 5) * 5;
}
}
return 100;
}
static void writeRingVolume(int ringVol) {
std::string cfg = getSxmoCfgDir();
::mkdir(cfg.c_str(), 0755);
std::string path = cfg + "/ringsettings";
int ringTime = 20;
int ringLoops = 10;
int vibStrength = 2;
{
std::ifstream ifs(path);
std::string line;
while (std::getline(ifs, line)) {
auto eq = line.find('=');
if (eq == std::string::npos) continue;
std::string k = line.substr(0, eq);
int val = std::atoi(line.substr(eq + 1).c_str());
if (k == "SXMO_RINGTIME" && val >= 5 && val <= 120) ringTime = val;
else if (k == "SXMO_RINGNUMBER" && val >= 1 && val <= 100) ringLoops = val;
else if (k == "SXMO_VIBRATE_STRENGTH" && val >= 1 && val <= 3) vibStrength = val;
}
}
std::ofstream ofs(path, std::ios::trunc);
if (ofs) {
ofs << "SXMO_RINGTIME=" << ringTime << "\n";
ofs << "SXMO_RINGNUMBER=" << ringLoops << "\n";
ofs << "SXMO_VIBRATE_STRENGTH=" << vibStrength << "\n";
ofs << "SXMO_RINGVOL=" << std::clamp(ringVol, 0, 100) << "\n";
}
}
void apply(ControlItem& it) {
switch (it.id) {
case ControlId::SpeakerVol: {
long hw = std::lround((it.value * 84.0) / 100.0);
std::string cfg = getSxmoCfgDir();
::mkdir(cfg.c_str(), 0755);
writeIntFile(cfg + "/speaker_vol", it.value);
long hw = (it.value <= 0) ? 0 : (60 + std::lround((it.value * 32.0) / 100.0));
if (it.value > 0) {
alsa_.setBoolean("SPK DAC Switch", true);
}
alsa_.setInteger("RX3 Digital Volume", hw);
char cmd[128];
char cmd[160];
std::snprintf(
cmd, sizeof(cmd),
"wpctl set-mute @DEFAULT_AUDIO_SINK@ %d >/dev/null 2>&1; "
"wpctl set-volume @DEFAULT_AUDIO_SINK@ %d%% >/dev/null 2>&1; "
"sxmo_hook_statusbar.sh audio >/dev/null 2>&1",
it.value);
"sxmo_hook_statusbar.sh volume >/dev/null 2>&1",
(it.value <= 0) ? 1 : 0, it.value);
runAsyncCmd(cmd);
break;
}
case ControlId::EarpieceVol: {
long hw = std::lround((it.value * 84.0) / 100.0);
long hw = (it.value <= 0) ? 0 : (58 + std::lround((it.value * 32.0) / 100.0));
alsa_.setInteger("RX1 Digital Volume", hw);
break;
}
case ControlId::RingVol: {
writeRingVolume(it.value);
break;
}
case ControlId::Flashlight: {
int v = it.value ? torchMax_ : 0;
if (!writeIntFile(torchDir_ + "/brightness", v)) {
@@ -994,7 +1071,9 @@ static int xdrawer_run_main(int argc, char** argv) {
case XK_4:
case XK_5:
case XK_6:
case XK_7: {
case XK_7:
case XK_8:
case XK_9: {
size_t idx = static_cast<size_t>(sym - XK_1);
if (idx < count) {
selected = idx;