#include "phone_data.h" #include "media_utils.h" #include "../sxmo-dwm/dwm_plugin.h" #include #include #include #include #include #include #include #include namespace { using namespace bananarchy; volatile sig_atomic_t gRunning = 1; void handleSignal(int) { gRunning = 0; } enum class ViewMode { Browser = 0, Actions = 1, TextView = 2, ImageView = 3, PropsView = 4, InputModal = 5, ConfirmDelete = 6, }; enum class SortMode { Name = 0, Date = 1, Size = 2, }; enum class InputAction { NewFolder = 0, NewFile = 1, Rename = 2, }; enum class TextCaseMode { Lower = 0, // abc Title = 1, // Abc Upper = 2, // ABC Num = 3, // 123 }; const char* kMultiTapFile[10] = { " _-0", // 0 ".-_1", // 1 "abc2", // 2 "def3", // 3 "ghi4", // 4 "jkl5", // 5 "mno6", // 6 "pqrs7", // 7 "tuv8", // 8 "wxyz9" // 9 }; struct FileItem { std::string name; std::string fullPath; std::string lowerName; std::string t9Name; bool isParent = false; bool isDir = false; bool isExec = false; off_t size = 0; time_t mtime = 0; mode_t mode = 0; }; void popUtf8(std::string& s) { if (s.empty()) return; while (!s.empty() && (static_cast(s.back()) & 0xC0) == 0x80) { s.pop_back(); } if (!s.empty()) s.pop_back(); } std::string getExtLower(const std::string& name) { size_t dot = name.rfind('.'); if (dot == std::string::npos || dot + 1 >= name.size()) { return ""; } return toLowerStr(name.substr(dot + 1)); } bool isImageExt(const std::string& ext) { return ext == "jpg" || ext == "jpeg" || ext == "png"; } bool isMediaExt(const std::string& ext) { return ext == "mp3" || ext == "wav" || ext == "ogg" || ext == "flac" || ext == "m4a" || ext == "mp4" || ext == "mkv" || ext == "webm" || ext == "avi"; } bool isLikelyTextFile(const std::string& path, off_t size) { if (size == 0) return true; if (size > 256 * 1024) return false; std::ifstream in(path, std::ios::binary); if (!in.is_open()) return false; char buf[512]; in.read(buf, sizeof(buf)); std::streamsize n = in.gcount(); for (std::streamsize i = 0; i < n; ++i) { unsigned char c = static_cast(buf[i]); if (c == 0) { return false; } } return true; } std::string shortenPath(const std::string& path) { const char* home = std::getenv("HOME"); std::string h = (home && home[0]) ? home : "/home/user"; std::string out = path; if (out == h) { out = "~"; } else if (out.rfind(h + "/", 0) == 0) { out = "~" + out.substr(h.size()); } if (out.size() > 18) { out = "..." + out.substr(out.size() - 15); } return out; } std::string formatPermissions(mode_t m) { std::string s(10, '-'); if (S_ISDIR(m)) s[0] = 'd'; else if (S_ISLNK(m)) s[0] = 'l'; if (m & S_IRUSR) s[1] = 'r'; if (m & S_IWUSR) s[2] = 'w'; if (m & S_IXUSR) s[3] = 'x'; if (m & S_IRGRP) s[4] = 'r'; if (m & S_IWGRP) s[5] = 'w'; if (m & S_IXGRP) s[6] = 'x'; if (m & S_IROTH) s[7] = 'r'; if (m & S_IWOTH) s[8] = 'w'; if (m & S_IXOTH) s[9] = 'x'; char oct[16]; std::snprintf(oct, sizeof(oct), " (%04o)", static_cast(m & 07777)); return s + oct; } bool removeRecursive(const std::string& path) { struct stat st {}; if (::lstat(path.c_str(), &st) != 0) { return false; } if (S_ISDIR(st.st_mode) && !S_ISLNK(st.st_mode)) { DIR* d = ::opendir(path.c_str()); if (d) { struct dirent* ent; while ((ent = ::readdir(d)) != nullptr) { if (std::strcmp(ent->d_name, ".") == 0 || std::strcmp(ent->d_name, "..") == 0) { continue; } removeRecursive(path + "/" + ent->d_name); } ::closedir(d); } return ::rmdir(path.c_str()) == 0; } return ::unlink(path.c_str()) == 0; } bool copyRecursive(const std::string& src, const std::string& dst) { pid_t pid = ::fork(); if (pid == 0) { execlp("cp", "cp", "-a", src.c_str(), dst.c_str(), static_cast(nullptr)); _exit(127); } else if (pid > 0) { int status = 0; ::waitpid(pid, &status, 0); return WIFEXITED(status) && WEXITSTATUS(status) == 0; } return false; } int runFilesApp(int argc, char** argv) { gRunning = 1; std::setlocale(LC_ALL, ""); XSetLocaleModifiers(""); bool isPreload = false; std::string initDir; for (int i = 1; i < argc; ++i) { if (!argv[i]) continue; if (std::strcmp(argv[i], "--preload") == 0) { isPreload = true; } else if (argv[i][0] != '-' && initDir.empty()) { initDir = argv[i]; } } std::string daemonPidPath = getPluginDaemonPidPath("files"); std::string activePidPath = getRuntimeFile("xfiles.pid"); if (isPluginDaemonAlive("files")) { if (!isPreload) { sendPluginCommand("files", initDir.empty() ? "OPEN" : ("OPEN " + initDir)); } return 0; } writeIntFile(daemonPidPath, static_cast(::getpid())); int fifoFd = setupPluginDaemonFifo("files"); 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 = loadThemeSpec(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[] = "xfiles"; char resClass[] = "XFiles"; ch.res_name = resName; ch.res_class = resClass; XSetClassHint(dpy, win, &ch); XStoreName(dpy, win, "Files"); 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& layoutIcon = pt.layoutIcon; auto& colWhite = pt.colWhite; auto& colLight = pt.colLight; auto& colMuted = pt.colMuted; auto& colYellow = pt.colYellow; auto& colGreen = pt.colGreen; auto& colBlue = pt.colBlue; auto& colOrange = pt.colOrange; auto& colRed = pt.colRed; const char* homeEnv = std::getenv("HOME"); std::string homeDir = (homeEnv && homeEnv[0]) ? homeEnv : "/home/user"; std::string currentDir = homeDir; ViewMode mode = ViewMode::Browser; SortMode sortMode = SortMode::Name; bool showHidden = false; std::string searchQuery; std::vector allItems; std::vector filteredItems; int selected = 0; int actionSelected = 0; int confirmSelected = 0; // Clipboard state std::string clipPath; std::string clipName; bool clipIsCut = false; // Text viewer state std::string viewingFilePath; std::string viewingFileName; std::vector textLines; int textScrollLine = 0; // Image viewer state constexpr int kImgCanvasW = 240; constexpr int kImgCanvasH = 232; std::vector imgCanvas(kImgCanvasW * kImgCanvasH, 0x111111); XImage* xImgPreview = XCreateImage( dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0, reinterpret_cast(imgCanvas.data()), kImgCanvasW, kImgCanvasH, 32, kImgCanvasW * 4); int imgOrigW = 0; int imgOrigH = 0; bool imgValid = false; // Modal input state (New Folder / New File / Rename) InputAction inputAction = InputAction::NewFolder; std::string inputTitle; std::string inputValue; std::string inputTargetPath; TextCaseMode caseMode = TextCaseMode::Lower; int lastTapDigit = -1; int lastTapCycle = 0; int64_t lastTapMs = 0; std::string statusBanner; int64_t statusBannerUntilMs = 0; struct ActionEntry { int id; std::string icon; std::string label; }; std::vector actionsList; bool visible = false; int64_t openStartMs = 0; int64_t startupIgnoreUntilMs = 0; bool initialSoftkeysReleased = true; int softkeyPending = 0; int64_t softkeyDeadlineMs = 0; auto updateFilter = [&]() { filteredItems.clear(); if (searchQuery.empty()) { filteredItems = allItems; } else { std::string qLower = toLowerStr(searchQuery); for (const auto& it : allItems) { if (it.isParent) continue; if (it.lowerName.find(qLower) != std::string::npos || it.t9Name.find(searchQuery) != std::string::npos) { filteredItems.push_back(it); } } } if (selected >= static_cast(filteredItems.size())) { selected = std::max(0, static_cast(filteredItems.size()) - 1); } }; auto loadDirectory = [&](const std::string& targetDir, const std::string& selectName = "") { char resolved[4096]; std::string dir = targetDir; if (::realpath(targetDir.c_str(), resolved) != nullptr) { dir = resolved; } DIR* d = ::opendir(dir.c_str()); if (!d) { statusBanner = "Cannot open directory"; statusBannerUntilMs = nowMs() + 1800; return; } currentDir = dir; allItems.clear(); if (currentDir != "/") { FileItem parent; parent.name = ".."; size_t slash = currentDir.rfind('/'); parent.fullPath = (slash == 0 || slash == std::string::npos) ? "/" : currentDir.substr(0, slash); parent.lowerName = ".."; parent.isParent = true; parent.isDir = true; allItems.push_back(parent); } std::vector dirs; std::vector files; struct dirent* ent; while ((ent = ::readdir(d)) != nullptr) { std::string name = ent->d_name; if (name == "." || name == "..") continue; if (!showHidden && !name.empty() && name[0] == '.') continue; FileItem item; item.name = name; item.fullPath = (currentDir == "/") ? ("/" + name) : (currentDir + "/" + name); item.lowerName = toLowerStr(name); item.t9Name = toT9Digits(name); struct stat st {}; if (::stat(item.fullPath.c_str(), &st) == 0) { item.isDir = S_ISDIR(st.st_mode); item.isExec = !item.isDir && ((st.st_mode & S_IXUSR) != 0); item.size = st.st_size; item.mtime = st.st_mtim.tv_sec; item.mode = st.st_mode; } if (item.isDir) { dirs.push_back(item); } else { files.push_back(item); } } ::closedir(d); auto cmp = [&](const FileItem& a, const FileItem& b) { if (sortMode == SortMode::Date && a.mtime != b.mtime) { return a.mtime > b.mtime; } if (sortMode == SortMode::Size && a.size != b.size) { return a.size > b.size; } return a.lowerName < b.lowerName; }; std::sort(dirs.begin(), dirs.end(), cmp); std::sort(files.begin(), files.end(), cmp); allItems.insert(allItems.end(), dirs.begin(), dirs.end()); allItems.insert(allItems.end(), files.begin(), files.end()); searchQuery.clear(); updateFilter(); selected = 0; if (!selectName.empty()) { for (size_t i = 0; i < filteredItems.size(); ++i) { if (filteredItems[i].name == selectName) { selected = static_cast(i); break; } } } }; auto openTextViewer = [&](const FileItem& item) { viewingFilePath = item.fullPath; viewingFileName = item.name; textLines.clear(); textScrollLine = 0; std::ifstream in(item.fullPath); std::string line; int maxLines = 1500; while (maxLines-- > 0 && std::getline(in, line)) { if (!line.empty() && line.back() == '\r') { line.pop_back(); } // Wrap lines longer than 30 chars for clean 240px display if (line.empty()) { textLines.push_back(""); } else { for (size_t pos = 0; pos < line.size(); pos += 30) { textLines.push_back(line.substr(pos, 30)); } } } if (textLines.empty()) { textLines.push_back("(Empty file)"); } mode = ViewMode::TextView; }; auto openImageViewer = [&](const FileItem& item) { viewingFilePath = item.fullPath; viewingFileName = item.name; std::vector rgb; imgOrigW = 0; imgOrigH = 0; imgValid = loadImageRgb24(item.fullPath, &rgb, &imgOrigW, &imgOrigH); if (imgValid) { scaleRgb24ToBgrxCanvas(rgb.data(), imgOrigW, imgOrigH, imgCanvas.data(), kImgCanvasW, kImgCanvasH, 0x111111); } else { std::fill(imgCanvas.begin(), imgCanvas.end(), 0x111111); } mode = ViewMode::ImageView; }; auto buildActionsList = [&]() { actionsList.clear(); bool hasSel = !filteredItems.empty(); bool isParent = hasSel && filteredItems[selected].isParent; if (hasSel) { actionsList.push_back({1, kIconEye, "Open / View"}); } if (!clipPath.empty()) { actionsList.push_back( {7, kIconCheck, std::string(clipIsCut ? "Move Here: " : "Paste Here: ") + clipName}); } actionsList.push_back({3, kIconPlus, "New Folder"}); actionsList.push_back({4, kIconFileText, "New Empty File"}); if (hasSel && !isParent) { if (!filteredItems[selected].isDir) { actionsList.push_back({2, kIconEdit, "Edit in nano"}); } actionsList.push_back({5, kIconEdit, "Rename"}); actionsList.push_back({6, kIconCopy, "Copy"}); actionsList.push_back({8, kIconOutCall, "Cut / Move"}); actionsList.push_back({9, kIconInfo, "Properties / Info"}); actionsList.push_back({13, kIconTrash, "Delete"}); } actionsList.push_back( {10, kIconEye, showHidden ? "Hide Dotfiles (.*)" : "Show Dotfiles (.*)"}); std::string sortLbl = "Sort: "; if (sortMode == SortMode::Name) sortLbl += "Date (#)"; else if (sortMode == SortMode::Date) sortLbl += "Size (#)"; else sortLbl += "Name (#)"; actionsList.push_back({11, kIconSort, sortLbl}); actionsList.push_back({12, kIconTerm, "Open Terminal Here"}); actionSelected = 0; }; auto openSelectedItem = [&]() { if (filteredItems.empty()) return; const FileItem item = filteredItems[selected]; if (item.isParent) { std::string prevBase = currentDir; size_t sl = prevBase.rfind('/'); std::string prevName = (sl != std::string::npos) ? prevBase.substr(sl + 1) : ""; loadDirectory(item.fullPath, prevName); return; } if (item.isDir) { loadDirectory(item.fullPath); return; } std::string ext = getExtLower(item.name); if (isImageExt(ext)) { openImageViewer(item); } else if (isMediaExt(ext)) { runAsyncArgv("sxmo_open.sh", item.fullPath.c_str()); statusBanner = "Opening in media player..."; statusBannerUntilMs = nowMs() + 1800; } else if (isLikelyTextFile(item.fullPath, item.size)) { openTextViewer(item); } else { buildActionsList(); mode = ViewMode::Actions; } }; auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, int h, const std::string& text, int maxW = -1) { pango_layout_set_text(layout, text.c_str(), static_cast(text.size())); 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); pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE); } int tw = 0, th = 0; pango_layout_get_pixel_size(layout, &tw, &th); int dy = y + std::max(0, (h - th) / 2); pango_xft_render_layout(xftDraw, color, layout, x * PANGO_SCALE, dy * PANGO_SCALE); return tw; }; auto drawSoftkeyBar = [&](const std::string& leftLabel, const std::string& centerLabel, const std::string& rightLabel) { const int barY = kWinHeight - 26; XSetForeground(dpy, gc, pt.spec.bgFooter); XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 26); XSetForeground(dpy, gc, pt.spec.divider); XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY); drawPango(layoutMain, &colBlue, 6, barY, 26, leftLabel); pango_layout_set_width(layoutMain, -1); pango_layout_set_text(layoutMain, centerLabel.c_str(), static_cast(centerLabel.size())); int cw = 0, chh = 0; pango_layout_get_pixel_size(layoutMain, &cw, &chh); drawPango(layoutMain, &colWhite, (kWinWidth - cw) / 2, barY, 26, centerLabel); pango_layout_set_width(layoutMain, -1); pango_layout_set_text(layoutMain, rightLabel.c_str(), static_cast(rightLabel.size())); int rw = 0, rhh = 0; pango_layout_get_pixel_size(layoutMain, &rw, &rhh); drawPango(layoutMain, &colLight, kWinWidth - rw - 6, barY, 26, rightLabel); }; auto drawUI = [&]() { if (!visible) return; pt.checkAndReload(); XSetForeground(dpy, gc, pt.spec.bgMain); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); if (mode == ViewMode::Browser) { XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 38); XSetForeground(dpy, gc, pt.spec.border); XDrawLine(dpy, pix, gc, 0, 37, kWinWidth, 37); if (searchQuery.empty()) { drawPango(layoutIcon, &colYellow, 6, 2, 34, kIconFolderOpen); drawPango(layoutMain, &colWhite, 28, 2, 34, shortenPath(currentDir), 156); const char* sTag = (sortMode == SortMode::Name) ? "N" : (sortMode == SortMode::Date ? "T" : "S"); std::string badge = std::string("[") + sTag + ":" + std::to_string(filteredItems.size()) + "]"; pango_layout_set_width(layoutSmall, -1); pango_layout_set_text(layoutSmall, badge.c_str(), static_cast(badge.size())); int bw = 0, bhh = 0; pango_layout_get_pixel_size(layoutSmall, &bw, &bhh); drawPango(layoutSmall, &colMuted, kWinWidth - bw - 6, 2, 34, badge); } else { drawPango(layoutIcon, &colBlue, 6, 2, 34, kIconSearch); drawPango(layoutMain, &colYellow, 28, 2, 34, searchQuery, 160); std::string cnt = std::to_string(filteredItems.size()); pango_layout_set_width(layoutSmall, -1); pango_layout_set_text(layoutSmall, cnt.c_str(), static_cast(cnt.size())); int bw = 0, bhh = 0; pango_layout_get_pixel_size(layoutSmall, &bw, &bhh); drawPango(layoutSmall, &colMuted, kWinWidth - bw - 6, 2, 34, cnt); } const int listY = 38; const int rowH = 33; const int visibleRows = 7; if (filteredItems.empty()) { drawPango(layoutMain, &colMuted, 16, 120, 24, "Empty folder", 208); } else { int scroll = 0; if (selected >= visibleRows) { scroll = selected - visibleRows + 1; } for (int i = 0; i < visibleRows; ++i) { int idx = scroll + i; if (idx >= static_cast(filteredItems.size())) break; const auto& it = filteredItems[idx]; int ry = listY + i * rowH; bool isSel = (idx == selected); if (isSel) { XSetForeground(dpy, gc, pt.spec.selBg); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); XSetForeground(dpy, gc, pt.spec.accentAlt); XDrawLine(dpy, pix, gc, 0, ry, kWinWidth, ry); } else { XSetForeground(dpy, gc, (i % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); } const char* ic = kIconFile; XftColor* icCol = &colMuted; if (it.isParent) { ic = kIconBackspace; icCol = &colBlue; } else if (it.isDir) { ic = kIconFolder; icCol = &colYellow; } else { std::string ext = getExtLower(it.name); if (isImageExt(ext)) { ic = kIconImage; icCol = &colGreen; } else if (isMediaExt(ext)) { ic = kIconMusic; icCol = &colBlue; } else if (it.isExec || ext == "sh") { ic = kIconTerm; icCol = &colOrange; } else { ic = kIconFileText; icCol = &colLight; } } drawPango(layoutIcon, icCol, 6, ry, rowH, ic); std::string dispName = it.isParent ? ".. (Up)" : (it.isDir ? (it.name + "/") : it.name); drawPango(layoutMain, &colWhite, 28, ry, rowH, dispName, 154); std::string meta = it.isParent ? "" : (it.isDir ? "DIR" : formatFileSize(it.size)); if (!meta.empty()) { pango_layout_set_width(layoutSmall, -1); pango_layout_set_text(layoutSmall, meta.c_str(), static_cast(meta.size())); int mw = 0, mhh = 0; pango_layout_get_pixel_size(layoutSmall, &mw, &mhh); drawPango(layoutSmall, isSel ? &colLight : &colMuted, kWinWidth - mw - 6, ry, rowH, meta); } } } drawSoftkeyBar("Actions", "Open", searchQuery.empty() ? "Close" : "Clear"); } else if (mode == ViewMode::Actions) { XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 34); drawPango(layoutIcon, &colBlue, 6, 0, 34, kIconList); drawPango(layoutMain, &colWhite, 28, 0, 34, "File Actions", 200); const int listY = 36; const int rowH = 33; const int visibleRows = 7; int scroll = 0; if (actionSelected >= visibleRows) { scroll = actionSelected - visibleRows + 1; } for (int i = 0; i < visibleRows; ++i) { int idx = scroll + i; if (idx >= static_cast(actionsList.size())) break; int ry = listY + i * rowH; bool isSel = (idx == actionSelected); XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgMain); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); drawPango(layoutIcon, isSel ? &colYellow : &colBlue, 8, ry, rowH, actionsList[idx].icon); drawPango(layoutMain, &colWhite, 32, ry, rowH, actionsList[idx].label, 202); } drawSoftkeyBar("Select", "Select", "Back"); } else if (mode == ViewMode::TextView) { XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 32); drawPango(layoutIcon, &colBlue, 6, 0, 32, kIconFileText); drawPango(layoutMain, &colWhite, 28, 0, 32, viewingFileName, 150); std::string posStr = std::to_string(textScrollLine + 1) + "/" + std::to_string(textLines.size()); pango_layout_set_width(layoutSmall, -1); pango_layout_set_text(layoutSmall, posStr.c_str(), static_cast(posStr.size())); int pw = 0, phh = 0; pango_layout_get_pixel_size(layoutSmall, &pw, &phh); drawPango(layoutSmall, &colMuted, kWinWidth - pw - 6, 0, 32, posStr); const int startY = 36; const int lineH = 18; const int maxVis = 12; for (int i = 0; i < maxVis; ++i) { int idx = textScrollLine + i; if (idx >= static_cast(textLines.size())) break; drawPango(layoutSmall, &colLight, 6, startY + i * lineH, lineH, textLines[idx], kWinWidth - 12); } 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); XPutImage(dpy, pix, gc, xImgPreview, 0, 0, 0, 36, kImgCanvasW, kImgCanvasH); drawSoftkeyBar("Full", "Next", "Back"); } else if (mode == ViewMode::PropsView && !filteredItems.empty()) { const auto& it = filteredItems[selected]; XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 34); drawPango(layoutIcon, &colBlue, 6, 0, 34, kIconInfo); drawPango(layoutMain, &colWhite, 28, 0, 34, "Properties", 200); drawPango(layoutSmall, &colMuted, 8, 42, 16, "Name:"); drawPango(layoutMain, &colWhite, 8, 58, 22, it.name, 224); drawPango(layoutSmall, &colMuted, 8, 86, 16, "Path:"); drawPango(layoutSmall, &colLight, 8, 102, 20, it.fullPath, 224); drawPango(layoutSmall, &colMuted, 8, 128, 16, "Size:"); drawPango(layoutMain, &colYellow, 8, 144, 22, formatFileSize(it.size) + " (" + std::to_string(it.size) + " B)", 224); drawPango(layoutSmall, &colMuted, 8, 172, 16, "Permissions:"); drawPango(layoutSmall, &colLight, 8, 188, 20, formatPermissions(it.mode), 224); struct tm tmM {}; localtime_r(&it.mtime, &tmM); char tbuf[32]; std::strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tmM); drawPango(layoutSmall, &colMuted, 8, 214, 16, "Modified:"); drawPango(layoutSmall, &colLight, 8, 230, 20, tbuf, 224); drawSoftkeyBar("Back", "Back", "Back"); } else if (mode == ViewMode::InputModal) { XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36); drawPango(layoutIcon, &colBlue, 6, 0, 36, kIconEdit); drawPango(layoutMain, &colWhite, 28, 0, 36, inputTitle, 145); const char* mBadge = "[abc]"; if (caseMode == TextCaseMode::Title) mBadge = "[Abc]"; else if (caseMode == TextCaseMode::Upper) mBadge = "[ABC]"; else if (caseMode == TextCaseMode::Num) mBadge = "[123]"; drawPango(layoutSmall, &colYellow, kWinWidth - 46, 0, 36, mBadge); XSetForeground(dpy, gc, pt.spec.bgInputFocus); XFillRectangle(dpy, pix, gc, 10, 74, kWinWidth - 20, 38); XSetForeground(dpy, gc, pt.spec.accentAlt); XDrawRectangle(dpy, pix, gc, 10, 74, kWinWidth - 21, 37); drawPango(layoutMain, &colWhite, 16, 74, 38, inputValue + "|", kWinWidth - 32); drawPango(layoutSmall, &colMuted, 10, 126, 20, "0-9: multi-tap #: cycle case", 220); drawSoftkeyBar("Save", "Save", inputValue.empty() ? "Cancel" : "Del"); } else if (mode == ViewMode::ConfirmDelete && !filteredItems.empty()) { const auto& it = filteredItems[selected]; XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36); drawPango(layoutIcon, &colRed, 8, 0, 36, kIconTrash); drawPango(layoutMain, &colWhite, 30, 0, 36, "Delete Item?", 200); drawPango(layoutMain, &colWhite, 14, 68, 26, it.name, 212); drawPango(layoutSmall, &colMuted, 14, 98, 22, it.isDir ? "Directory and all contents" : "File will be permanently", 212); drawPango(layoutSmall, &colMuted, 14, 118, 22, it.isDir ? "will be deleted!" : "deleted.", 212); for (int i = 0; i < 2; ++i) { int ry = 164 + i * 40; bool isSel = (confirmSelected == i); XSetForeground( dpy, gc, isSel ? (i == 1 ? pt.spec.dangerBg : pt.spec.selBg) : pt.spec.bgButton); XFillRectangle(dpy, pix, gc, 16, ry, kWinWidth - 32, 34); drawPango(layoutMain, &colWhite, 28, ry, 34, i == 0 ? "No, Keep Item" : "Yes, Delete", 184); } drawSoftkeyBar("Select", "Confirm", "Cancel"); } int64_t tNow = nowMs(); if (!statusBanner.empty() && tNow < statusBannerUntilMs) { XSetForeground(dpy, gc, pt.spec.bgToast); XFillRectangle(dpy, pix, gc, 8, kWinHeight - 58, kWinWidth - 16, 28); XSetForeground(dpy, gc, pt.spec.info); XDrawRectangle(dpy, pix, gc, 8, kWinHeight - 58, kWinWidth - 17, 27); drawPango(layoutSmall, &colWhite, 14, kWinHeight - 58, 28, statusBanner, kWinWidth - 28); } XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); XFlush(dpy); }; auto showWindow = [&](const std::string& dirArg) { closeOtherPlugins("files"); mode = ViewMode::Browser; loadDirectory(dirArg.empty() ? currentDir : dirArg); writeIntFile(activePidPath, static_cast(::getpid())); if (!visible) { visible = true; XMapRaised(dpy, win); for (int i = 0; i < 20; ++i) { if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime) == GrabSuccess) { break; } usleep(2000); } } else { XRaiseWindow(dpy, win); } XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime); openStartMs = nowMs(); startupIgnoreUntilMs = openStartMs + 180; initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); softkeyPending = 0; drawUI(); }; auto hideWindow = [&]() { if (!visible) return; writeSoftkeySuppress(nowMs() + 450); XUngrabKeyboard(dpy, CurrentTime); XUnmapWindow(dpy, win); XFlush(dpy); visible = false; ::unlink(activePidPath.c_str()); }; auto commitModalTap = [&](int digit) { const char* chars = kMultiTapFile[digit]; int len = static_cast(std::strlen(chars)); if (len <= 0) return; int64_t t = nowMs(); if (caseMode == TextCaseMode::Num) { inputValue.push_back(static_cast('0' + digit)); lastTapDigit = -1; return; } bool sameKey = (lastTapDigit == digit && (t - lastTapMs) <= 850 && !inputValue.empty()); if (sameKey) { lastTapCycle = (lastTapCycle + 1) % len; popUtf8(inputValue); } else { lastTapDigit = digit; lastTapCycle = 0; } lastTapMs = t; char ch = chars[lastTapCycle]; if (std::isalpha(static_cast(ch))) { if (caseMode == TextCaseMode::Upper) { ch = static_cast(std::toupper(static_cast(ch))); } else if (caseMode == TextCaseMode::Title && inputValue.empty()) { ch = static_cast(std::toupper(static_cast(ch))); } } inputValue.push_back(ch); }; auto executeAction = [&](int actId) { if (actId == 1) { mode = ViewMode::Browser; openSelectedItem(); } else if (actId == 2 && !filteredItems.empty()) { std::string p = filteredItems[selected].fullPath; hideWindow(); runAsyncCmd("st -e nano \"" + p + "\""); } else if (actId == 3) { inputAction = InputAction::NewFolder; inputTitle = "New Folder"; inputValue.clear(); lastTapDigit = -1; mode = ViewMode::InputModal; } else if (actId == 4) { inputAction = InputAction::NewFile; inputTitle = "New File"; inputValue.clear(); lastTapDigit = -1; mode = ViewMode::InputModal; } else if (actId == 5 && !filteredItems.empty()) { inputAction = InputAction::Rename; inputTitle = "Rename"; inputValue = filteredItems[selected].name; inputTargetPath = filteredItems[selected].fullPath; lastTapDigit = -1; mode = ViewMode::InputModal; } else if (actId == 6 && !filteredItems.empty()) { clipPath = filteredItems[selected].fullPath; clipName = filteredItems[selected].name; clipIsCut = false; statusBanner = "Copied: " + clipName; statusBannerUntilMs = nowMs() + 1800; mode = ViewMode::Browser; } else if (actId == 8 && !filteredItems.empty()) { clipPath = filteredItems[selected].fullPath; clipName = filteredItems[selected].name; clipIsCut = true; statusBanner = "Cut: " + clipName; statusBannerUntilMs = nowMs() + 1800; mode = ViewMode::Browser; } else if (actId == 7 && !clipPath.empty()) { std::string dst = (currentDir == "/") ? ("/" + clipName) : (currentDir + "/" + clipName); bool ok = false; if (clipIsCut) { ok = (::rename(clipPath.c_str(), dst.c_str()) == 0); if (!ok && copyRecursive(clipPath, dst)) { removeRecursive(clipPath); ok = true; } if (ok) clipPath.clear(); } else { ok = copyRecursive(clipPath, dst); } statusBanner = ok ? "Paste complete" : "Paste failed"; statusBannerUntilMs = nowMs() + 1800; loadDirectory(currentDir, clipName); mode = ViewMode::Browser; } else if (actId == 9 && !filteredItems.empty()) { mode = ViewMode::PropsView; } else if (actId == 10) { showHidden = !showHidden; loadDirectory(currentDir); mode = ViewMode::Browser; } else if (actId == 11) { sortMode = static_cast((static_cast(sortMode) + 1) % 3); loadDirectory(currentDir); mode = ViewMode::Browser; } else if (actId == 12) { std::string d = currentDir; hideWindow(); runAsyncCmd("cd \"" + d + "\" && exec st"); } else if (actId == 13 && !filteredItems.empty()) { confirmSelected = 0; mode = ViewMode::ConfirmDelete; } }; auto stepImageInFolder = [&](int delta) { if (filteredItems.empty()) return; int n = static_cast(filteredItems.size()); for (int step = 1; step <= n; ++step) { int idx = (selected + delta * step + n * 2) % n; if (!filteredItems[idx].isDir && isImageExt(getExtLower(filteredItems[idx].name))) { selected = idx; openImageViewer(filteredItems[selected]); return; } } }; auto handleSoftkeyAction = [&](bool isLeft) { if (mode == ViewMode::Browser) { if (isLeft) { buildActionsList(); mode = ViewMode::Actions; } else { if (!searchQuery.empty()) { popUtf8(searchQuery); updateFilter(); } else { hideWindow(); return; } } } else if (mode == ViewMode::Actions) { if (isLeft) { if (!actionsList.empty()) { executeAction(actionsList[actionSelected].id); } } else { mode = ViewMode::Browser; } } else if (mode == ViewMode::TextView) { if (isLeft) { std::string p = viewingFilePath; hideWindow(); runAsyncCmd("st -e nano \"" + p + "\""); return; } else { mode = ViewMode::Browser; } } else if (mode == ViewMode::ImageView) { if (isLeft) { runAsyncArgv("feh", viewingFilePath.c_str()); } else { mode = ViewMode::Browser; } } else if (mode == ViewMode::PropsView) { mode = ViewMode::Browser; } else if (mode == ViewMode::InputModal) { if (isLeft) { std::string name = trimStr(inputValue); if (!name.empty() && name.find('/') == std::string::npos) { std::string target = (currentDir == "/") ? ("/" + name) : (currentDir + "/" + name); if (inputAction == InputAction::NewFolder) { ::mkdir(target.c_str(), 0755); } else if (inputAction == InputAction::NewFile) { int fd = ::open(target.c_str(), O_WRONLY | O_CREAT, 0644); if (fd >= 0) ::close(fd); } else if (inputAction == InputAction::Rename) { ::rename(inputTargetPath.c_str(), target.c_str()); } loadDirectory(currentDir, name); } mode = ViewMode::Browser; } else { if (!inputValue.empty()) { popUtf8(inputValue); lastTapDigit = -1; } else { mode = ViewMode::Browser; } } } else if (mode == ViewMode::ConfirmDelete) { if (isLeft) { if (confirmSelected == 1 && !filteredItems.empty()) { removeRecursive(filteredItems[selected].fullPath); loadDirectory(currentDir); } mode = ViewMode::Browser; } else { mode = ViewMode::Browser; } } drawUI(); }; if (!isPreload) { showWindow(initDir); } int xfd = ConnectionNumber(dpy); while (gRunning) { for (const auto& cmd : readFifoLines(fifoFd)) { if (cmd == "NOP") { continue; } else if (cmd == "RELOAD_THEME") { pt.reload(); if (visible) drawUI(); } else if (cmd == "CLOSE") { hideWindow(); } else if (cmd == "OPEN") { showWindow(""); } else if (cmd.rfind("OPEN ", 0) == 0) { showWindow(trimStr(cmd.substr(5))); } } while (XPending(dpy) > 0) { XEvent ev; XNextEvent(dpy, &ev); if (!visible) continue; if (ev.type == Expose && ev.xexpose.count == 0) { drawUI(); } else if (ev.type == KeyPress) { int64_t tNow = nowMs(); unsigned int kc = ev.xkey.keycode; KeySym sym = XLookupKeysym(&ev.xkey, 0); bool isLeftSk = (kc == 135 || sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 || sym == _EVDEVK(0x246) || sym == XK_F1); bool isRightSk = (kc == 166 || sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 || sym == _EVDEVK(0x247) || sym == XK_F2); if (!initialSoftkeysReleased) { if (querySoftkeysDown(dpy) == 0) { initialSoftkeysReleased = true; } else if (isLeftSk || isRightSk) { continue; } } if (tNow < startupIgnoreUntilMs && (isLeftSk || isRightSk)) { continue; } if (isLeftSk || isRightSk) { int bit = isLeftSk ? 1 : 2; softkeyPending |= bit; if (softkeyPending == 3 || querySoftkeysDown(dpy) == 3) { softkeyPending = 0; sendPluginCommand("xdrawer", "TOGGLE"); continue; } softkeyDeadlineMs = tNow + 35; continue; } if (sym == XK_Escape || kc == 124 || sym == XF86XK_PowerOff) { hideWindow(); continue; } if (sym == XK_BackSpace || sym == XK_Delete) { if (mode == ViewMode::Browser && searchQuery.empty() && currentDir != "/") { std::string prevBase = currentDir; size_t sl = prevBase.rfind('/'); std::string prevName = (sl != std::string::npos) ? prevBase.substr(sl + 1) : ""; std::string upDir = (sl == 0 || sl == std::string::npos) ? "/" : prevBase.substr(0, sl); loadDirectory(upDir, prevName); drawUI(); } else { handleSoftkeyAction(false); } continue; } if (mode == ViewMode::Browser) { if (sym == XK_Up || sym == XK_KP_Up) { if (!filteredItems.empty()) { selected = (selected - 1 + static_cast(filteredItems.size())) % static_cast(filteredItems.size()); drawUI(); } } else if (sym == XK_Down || sym == XK_KP_Down) { if (!filteredItems.empty()) { selected = (selected + 1) % static_cast(filteredItems.size()); drawUI(); } } else if (sym == XK_Left || sym == XK_KP_Left) { if (currentDir != "/") { std::string prevBase = currentDir; size_t sl = prevBase.rfind('/'); std::string prevName = (sl != std::string::npos) ? prevBase.substr(sl + 1) : ""; std::string upDir = (sl == 0 || sl == std::string::npos) ? "/" : prevBase.substr(0, sl); loadDirectory(upDir, prevName); drawUI(); } } else if (sym == XK_Right || sym == XK_KP_Right || sym == XK_Return || sym == XK_KP_Enter) { openSelectedItem(); drawUI(); } else { char d = mapDialKey(sym, kc, ev.xkey.state); if (d == '*') { showHidden = !showHidden; loadDirectory(currentDir); drawUI(); } else if (d == '#') { sortMode = static_cast((static_cast(sortMode) + 1) % 3); loadDirectory(currentDir); drawUI(); } else if (d >= '0' && d <= '9') { searchQuery.push_back(d); updateFilter(); drawUI(); } } } else if (mode == ViewMode::Actions) { if (sym == XK_Up || sym == XK_KP_Up) { if (!actionsList.empty()) { actionSelected = (actionSelected - 1 + static_cast(actionsList.size())) % static_cast(actionsList.size()); drawUI(); } } else if (sym == XK_Down || sym == XK_KP_Down) { if (!actionsList.empty()) { actionSelected = (actionSelected + 1) % static_cast(actionsList.size()); drawUI(); } } else if (sym == XK_Return || sym == XK_KP_Enter) { handleSoftkeyAction(true); } } else if (mode == ViewMode::TextView) { if (sym == XK_Up || sym == XK_KP_Up) { textScrollLine = std::max(0, textScrollLine - 1); drawUI(); } else if (sym == XK_Down || sym == XK_KP_Down || sym == XK_Return || sym == XK_KP_Enter) { textScrollLine = std::min( std::max(0, static_cast(textLines.size()) - 1), textScrollLine + 1); drawUI(); } else if (sym == XK_Left || sym == XK_KP_Left || sym == XK_Prior) { textScrollLine = std::max(0, textScrollLine - 10); drawUI(); } else if (sym == XK_Right || sym == XK_KP_Right || sym == XK_Next) { textScrollLine = std::min( std::max(0, static_cast(textLines.size()) - 1), textScrollLine + 10); drawUI(); } } else if (mode == ViewMode::ImageView) { 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 || sym == XK_KP_Down || sym == XK_Return || sym == XK_KP_Enter) { stepImageInFolder(1); drawUI(); } } else if (mode == ViewMode::PropsView) { if (sym == XK_Return || sym == XK_KP_Enter) { mode = ViewMode::Browser; drawUI(); } } else if (mode == ViewMode::InputModal) { if (sym == XK_Return || sym == XK_KP_Enter) { handleSoftkeyAction(true); } else if (sym == XK_Right || sym == XK_KP_Right) { lastTapDigit = -1; } else { char d = mapDialKey(sym, kc, ev.xkey.state); if (d == '#') { lastTapDigit = -1; caseMode = static_cast( (static_cast(caseMode) + 1) % 4); drawUI(); } else if (d == '*') { commitModalTap(1); drawUI(); } else if (d >= '0' && d <= '9') { commitModalTap(d - '0'); drawUI(); } } } else if (mode == ViewMode::ConfirmDelete) { if (sym == XK_Up || sym == XK_Down || sym == XK_KP_Up || sym == XK_KP_Down) { confirmSelected = 1 - confirmSelected; drawUI(); } else if (sym == XK_Return || sym == XK_KP_Enter) { handleSoftkeyAction(true); } } } } int64_t tNow = nowMs(); if (visible && softkeyPending != 0 && tNow >= softkeyDeadlineMs) { if (querySoftkeysDown(dpy) == 3) { softkeyPending = 0; sendPluginCommand("xdrawer", "TOGGLE"); } else { bool isLeft = (softkeyPending == 1); softkeyPending = 0; handleSoftkeyAction(isLeft); } } int timeoutMs = -1; if (visible && softkeyPending != 0) { timeoutMs = std::max(1, static_cast(softkeyDeadlineMs - tNow)); } struct pollfd pfds[2]; int nfds = 0; pfds[nfds++] = {xfd, POLLIN, 0}; int fifoIdx = -1; if (fifoFd >= 0) { fifoIdx = nfds; pfds[nfds++] = {fifoFd, POLLIN, 0}; } int pr = ::poll(pfds, nfds, timeoutMs); if (pr > 0 && fifoIdx >= 0 && (pfds[fifoIdx].revents & POLLIN)) { for (const auto& cmd : readFifoLines(fifoFd)) { if (cmd == "RELOAD_THEME") { pt.reload(); if (visible) drawUI(); } else if (cmd == "CLOSE") { hideWindow(); } else if (cmd == "OPEN") { showWindow(""); } else if (cmd.rfind("OPEN ", 0) == 0) { showWindow(trimStr(cmd.substr(5))); } } } } hideWindow(); if (xImgPreview) { xImgPreview->data = nullptr; XDestroyImage(xImgPreview); } if (fifoFd >= 0) ::close(fifoFd); ::unlink(daemonPidPath.c_str()); return 0; } int files_plugin_init(const DwmHostApi* host) { if (host && host->spawn_plugin) { host->spawn_plugin("files", "--preload"); } return 0; } } // namespace extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { DWM_PLUGIN_API_VERSION, "files", "Keypad-driven graphical file manager with text/image preview", files_plugin_init, nullptr, nullptr, nullptr, nullptr, runFilesApp };