2026-10-21 14:46:11 +03:00
|
|
|
#include "phone_data.h"
|
|
|
|
|
#include "media_utils.h"
|
|
|
|
|
#include "../sxmo-dwm/dwm_plugin.h"
|
|
|
|
|
|
|
|
|
|
#include <X11/Xatom.h>
|
|
|
|
|
#include <X11/Xft/Xft.h>
|
|
|
|
|
#include <X11/Xutil.h>
|
|
|
|
|
#include <pango/pangoxft.h>
|
|
|
|
|
#include <poll.h>
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <sys/wait.h>
|
|
|
|
|
#include <clocale>
|
|
|
|
|
|
|
|
|
|
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<unsigned char>(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<unsigned char>(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<unsigned int>(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<char*>(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");
|
|
|
|
|
|
2026-10-21 16:00:41 +03:00
|
|
|
if (isPluginDaemonAlive("files")) {
|
2026-10-21 14:46:11 +03:00
|
|
|
if (!isPreload) {
|
|
|
|
|
sendPluginCommand("files", initDir.empty() ? "OPEN" : ("OPEN " + initDir));
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writeIntFile(daemonPidPath, static_cast<int>(::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);
|
|
|
|
|
|
|
|
|
|
XSetWindowAttributes swa {};
|
|
|
|
|
swa.override_redirect = True;
|
|
|
|
|
swa.background_pixel = 0x222222;
|
|
|
|
|
swa.border_pixel = 0x005577;
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
PangoFontMap* fontMap = pango_xft_get_font_map(dpy, screen);
|
|
|
|
|
PangoContext* pangoCtx = pango_font_map_create_context(fontMap);
|
|
|
|
|
|
|
|
|
|
PangoLayout* layoutMain = pango_layout_new(pangoCtx);
|
|
|
|
|
PangoFontDescription* descMain =
|
|
|
|
|
pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 15px");
|
|
|
|
|
pango_layout_set_font_description(layoutMain, descMain);
|
|
|
|
|
|
|
|
|
|
PangoLayout* layoutSmall = pango_layout_new(pangoCtx);
|
|
|
|
|
PangoFontDescription* descSmall =
|
|
|
|
|
pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 12px");
|
|
|
|
|
pango_layout_set_font_description(layoutSmall, descSmall);
|
|
|
|
|
|
|
|
|
|
PangoLayout* layoutIcon = pango_layout_new(pangoCtx);
|
|
|
|
|
PangoFontDescription* descIcon =
|
|
|
|
|
pango_font_description_from_string("DejaVuSansM Nerd Font, Fira Code 16px");
|
|
|
|
|
pango_layout_set_font_description(layoutIcon, descIcon);
|
|
|
|
|
|
|
|
|
|
auto allocColor = [&](const char* hex) {
|
|
|
|
|
XftColor col {};
|
|
|
|
|
XftColorAllocName(dpy, visual, cmap, hex, &col);
|
|
|
|
|
return col;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
XftColor colWhite = allocColor("#ffffff");
|
|
|
|
|
XftColor colLight = allocColor("#dddddd");
|
|
|
|
|
XftColor colMuted = allocColor("#999999");
|
|
|
|
|
XftColor colYellow = allocColor("#f4d03f");
|
|
|
|
|
XftColor colGreen = allocColor("#2ecc71");
|
|
|
|
|
XftColor colBlue = allocColor("#5dade2");
|
|
|
|
|
XftColor colOrange = allocColor("#e67e22");
|
|
|
|
|
XftColor colRed = allocColor("#e74c3c");
|
|
|
|
|
|
|
|
|
|
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<FileItem> allItems;
|
|
|
|
|
std::vector<FileItem> 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<std::string> textLines;
|
|
|
|
|
int textScrollLine = 0;
|
|
|
|
|
|
|
|
|
|
// Image viewer state
|
|
|
|
|
constexpr int kImgCanvasW = 240;
|
|
|
|
|
constexpr int kImgCanvasH = 232;
|
|
|
|
|
std::vector<uint32_t> imgCanvas(kImgCanvasW * kImgCanvasH, 0x111111);
|
|
|
|
|
XImage* xImgPreview = XCreateImage(
|
|
|
|
|
dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0,
|
|
|
|
|
reinterpret_cast<char*>(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<ActionEntry> 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<int>(filteredItems.size())) {
|
|
|
|
|
selected = std::max(0, static_cast<int>(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<FileItem> dirs;
|
|
|
|
|
std::vector<FileItem> 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<int>(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<uint8_t> 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<int>(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, 0x141414);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 26);
|
|
|
|
|
XSetForeground(dpy, gc, 0x2d2d2d);
|
|
|
|
|
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<int>(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<int>(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;
|
|
|
|
|
|
|
|
|
|
XSetForeground(dpy, gc, 0x222222);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
|
|
|
|
|
|
|
|
|
|
if (mode == ViewMode::Browser) {
|
|
|
|
|
XSetForeground(dpy, gc, 0x161616);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 38);
|
|
|
|
|
XSetForeground(dpy, gc, 0x333333);
|
|
|
|
|
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<int>(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<int>(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<int>(filteredItems.size())) break;
|
|
|
|
|
const auto& it = filteredItems[idx];
|
|
|
|
|
int ry = listY + i * rowH;
|
|
|
|
|
bool isSel = (idx == selected);
|
|
|
|
|
|
|
|
|
|
if (isSel) {
|
|
|
|
|
XSetForeground(dpy, gc, 0x005577);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1);
|
|
|
|
|
XSetForeground(dpy, gc, 0x0088bb);
|
|
|
|
|
XDrawLine(dpy, pix, gc, 0, ry, kWinWidth, ry);
|
|
|
|
|
} else {
|
|
|
|
|
XSetForeground(dpy, gc, (i % 2 == 0) ? 0x222222 : 0x1d1d1d);
|
|
|
|
|
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<int>(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, 0x161616);
|
|
|
|
|
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<int>(actionsList.size())) break;
|
|
|
|
|
int ry = listY + i * rowH;
|
|
|
|
|
bool isSel = (idx == actionSelected);
|
|
|
|
|
XSetForeground(dpy, gc, isSel ? 0x005577 : 0x222222);
|
|
|
|
|
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, 0x161616);
|
|
|
|
|
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<int>(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<int>(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, 0x161616);
|
|
|
|
|
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, 0x161616);
|
|
|
|
|
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, 0x161616);
|
|
|
|
|
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, 0x182630);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 10, 74, kWinWidth - 20, 38);
|
|
|
|
|
XSetForeground(dpy, gc, 0x0088bb);
|
|
|
|
|
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, 0x161616);
|
|
|
|
|
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 ? 0x922b21 : 0x005577) : 0x2a2a2a);
|
|
|
|
|
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, 0x102535);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 8, kWinHeight - 58, kWinWidth - 16, 28);
|
|
|
|
|
XSetForeground(dpy, gc, 0x5dade2);
|
|
|
|
|
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<int>(::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<int>(std::strlen(chars));
|
|
|
|
|
if (len <= 0) return;
|
|
|
|
|
|
|
|
|
|
int64_t t = nowMs();
|
|
|
|
|
if (caseMode == TextCaseMode::Num) {
|
|
|
|
|
inputValue.push_back(static_cast<char>('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<unsigned char>(ch))) {
|
|
|
|
|
if (caseMode == TextCaseMode::Upper) {
|
|
|
|
|
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
|
|
|
|
} else if (caseMode == TextCaseMode::Title && inputValue.empty()) {
|
|
|
|
|
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(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<SortMode>((static_cast<int>(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<int>(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 == "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<int>(filteredItems.size())) %
|
|
|
|
|
static_cast<int>(filteredItems.size());
|
|
|
|
|
drawUI();
|
|
|
|
|
}
|
|
|
|
|
} else if (sym == XK_Down || sym == XK_KP_Down) {
|
|
|
|
|
if (!filteredItems.empty()) {
|
|
|
|
|
selected =
|
|
|
|
|
(selected + 1) % static_cast<int>(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<SortMode>((static_cast<int>(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<int>(actionsList.size())) %
|
|
|
|
|
static_cast<int>(actionsList.size());
|
|
|
|
|
drawUI();
|
|
|
|
|
}
|
|
|
|
|
} else if (sym == XK_Down || sym == XK_KP_Down) {
|
|
|
|
|
if (!actionsList.empty()) {
|
|
|
|
|
actionSelected =
|
|
|
|
|
(actionSelected + 1) % static_cast<int>(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<int>(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<int>(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<TextCaseMode>(
|
|
|
|
|
(static_cast<int>(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<int>(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 == "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
|
|
|
|
|
};
|