feat(xfiles): add Yabasic .yab terminal execution and integrated GUI text editor

This commit is contained in:
Luxferre
2026-10-26 16:08:35 +02:00
parent 6db14be799
commit 73ba2d5c22
6 changed files with 668 additions and 12 deletions
+3 -1
View File
@@ -20,8 +20,10 @@ Release versions follow the `<year-letter><MM><DD>` format (where `2026` is `a`,
- Readjustable keypad controls persisted in `~/.config/sxmo/gambatte.cfg` with defaults `B = 8`, `Turbo B = 5`, `A = 9`, `Turbo A = 6`, `Directions = D-Pad`, `Start = 2`, `Select = 3`, `Palette = *`, and `Fast Forward = 0`.
- 10 built-in DMG color palettes, 4 screen scaling modes (`Fit 240x216` 1.5x area-interpolated, `1:1 160x144`, `Fill 240x246`, `Full 240x296`), 10 save-state slots (`.st0`–`.st9`), automatic battery SRAM/RTC persistence, seamless `Xdrawer` focus handoff, and direct low-latency `libasound` (`hw:0,0`) audio with hardware volume synchronization and analog-stage low-pass filtering.
- Preinstalled open-source / public-domain GB and GBC games in `/usr/share/games/gb/` and `~/Games/` (`2048.gb`, `adjustris.gb`, `blastah.gb`, `brickster.gbc`, `geometrix.gbc`, `ucity.gbc`).
- **Preinstalled scripting and calculation tools**:
- **Preinstalled scripting, calculation tools, and File Manager (`xfiles`) integration**:
- Added **Lua** (`lua5.5` and `lua5.4` with `/usr/bin/lua` and `/usr/bin/luac`), **GNU `bc` / `dc`**, and **Yabasic (`yabasic`)** to the default system image.
- Selecting any `.yab` file in **File Manager (`xfiles`)** (or choosing **Run in Yabasic (Terminal)** in its Actions menu) runs `yabasic` inside the `st` terminal and pauses on exit so output can be inspected.
- Added an **integrated GUI Text Editor (`Edit in Text Editor`)** inside `xfiles`, available as an action on any file (and automatically opened when creating a **New File** or pressing **Edit** in the text viewer), featuring line numbers, 2D cursor navigation, 5 multi-tap input modes (`[abc]`, `[Abc]`, `[ABC]`, `[123]`, `[Sym]`), live candidate bar, and an **Editor Menu** (`Save File`, `Save & Close`, `Insert New Line`, `Insert Space`, `Duplicate Line`, `Delete Line`, `Close Without Saving`).
---
+6 -1
View File
@@ -169,10 +169,15 @@ All fifteen phone applications are compiled as shared-object plugins (`/usr/lib/
8. **File manager (`files.so` / `xfiles`)**:
- **Open**: select **Files** from the System menu or press `Mod4+d`.
- **Controls and viewers**: navigate directories with `Up`/`Down`/`Left`/`Right`/`Return`, filter with `0`–`9`, toggle dotfiles with `*`, and cycle sort order (**Name** / **Date** / **Size**) with `#`.
- **Integrated GUI Text Editor (`Edit in Text Editor`)**: available as an action in the **Left softkey (`Actions`)** menu for **any non-directory file** (as well as via **Left softkey (`Edit`)** from the built-in text viewer, or automatically after **New File**):
- **Navigation**: `Up`/`Down`/`Left`/`Right` moves the cursor across lines and UTF-8 characters with automatic vertical and horizontal viewport scrolling and line numbers (`Ln:Col`).
- **Multi-tap keypad text entry**: type with `0`–`9` and `*` (with a live bottom candidate bar), press `#` to cycle input modes (`[abc]` -> `[Abc]` -> `[ABC]` -> `[123]` -> `[Sym]`), press **Center `OK` (`Return`)** to split the line / insert a newline, and press **Right softkey (`Del`)** or `BackSpace` to delete the character before the cursor (or join with the previous line at column `0`).
- **Editor Menu (`Left softkey`)**: opens the **Editor Menu** with **Save File** (press **Left softkey** twice to save immediately), **Save & Close**, **Insert New Line**, **Insert Space**, **Duplicate Line**, **Delete Line**, and **Close Without Saving**.
- **Yabasic (`.yab`) script execution**: pressing `Return` on any `.yab` file (or selecting **Run in Yabasic (Terminal)** from the **Left softkey (`Actions`)** menu) launches `yabasic` inside `st` in the file's directory and waits for `Enter` after completion so output can be inspected.
- **Game Boy / Game Boy Color ROM launching**: pressing `Return` on any `.gb` or `.gbc` file (or selecting **Play in Game Boy Emulator** from the **Left softkey (`Actions`)** menu) opens it directly in the **Gambatte (`gambatte`)** emulator.
- **Image preview and wallpaper installation**: pressing `Return` on any image file (`.jpg`, `.png`, `.bmp`, `.gif`, `.webp`, `.ppm`, `.tif`, `.ico`, `.svg`) opens the built-in image viewer (`*` toggles full-screen preview; **Left softkey (`Set Wall`)** installs the image into `~/Pictures/Wallpapers/`, applies it via `feh`, and refreshes Conky). You can also select **Preview Image** or **Set as Wallpaper** directly from the **Left softkey (`Actions`)** menu.
- **Audio playback and ringtone installation**: pressing `Return` on any audio or tracker module file opens it immediately in **Music (`xmusic`)**. From the **Left softkey (`Actions`)** menu on any audio/tracker file, select **Play in Music Player** to play it in `xmusic`, or **Set as Ringtone** to copy it to `~/Ringtones/` and set it as the active phone ringtone.
- **File management actions**: the **Left softkey (`Actions`)** menu also provides **View Text**, **Edit in Nano**, **New Folder**, **New File**, **Rename**, **Copy**, **Cut / Move**, **Paste Here**, **Properties**, **Open Terminal Here**, and **Delete**.
- **File management actions**: the **Left softkey (`Actions`)** menu also provides **Edit in Text Editor**, **View Text**, **Edit in nano**, **New Folder**, **New File**, **Rename**, **Copy**, **Cut / Move**, **Paste Here**, **Properties**, **Open Terminal Here**, and **Delete**.
9. **Camera (`camera.so` / `xcamera`)**:
- **Open**: select **Camera** from the System menu or run `xcamera`.
+1 -1
View File
@@ -26,7 +26,7 @@ Instead of spawning slow shell scripts for everyday phone tasks, Bananarchy runs
- **Contacts (`xcontacts`) and SMS manager (`xsms`)**: full phonebook editor with direct Qualcomm QMI SIM card contact import and export across SIM 1 and SIM 2, paired with a threaded SMS conversation view, multi-tap message composer, and built-in anti-spam blocklist / `Spam Folder` (`~/.local/share/sxmo/block`) with one-key thread blocking and restoration.
- **Modem and Dual-SIM manager (`xmodem`)**: 4-tab cellular control center (`SIMs`, `Radio`, `AT`, `Log`) supporting physical SIM 1 / SIM 2 active slot switching, SIM PIN unlock and PUK unblock wizards, 2G/3G/4G radio mode selection, interactive USSD sessions, and a live AT command console.
- **Networks and Bluetooth manager (`xnetworks`)**: 3-tab connectivity manager for Wi-Fi networks, WPA2 password entry, Wi-Fi hotspot, USB tethering, 4G/LTE mobile data (`wwan0`) and APN editing, and Bluetooth pairing.
- **File manager (`xfiles`), music and tracker player (`xmusic`), and camera (`xcamera`)**: keypad directory browser with built-in text and image viewers (plus one-click actions to launch `.gb`/`.gbc` ROMs in the Game Boy emulator, play audio, preview images, set any image as wallpaper, or install any audio or tracker file as ringtone), a 3-tab music and chiptune tracker module player (`WAV`, `MP3`, `OGG`, `FLAC`, `Opus`, `MOD`, `XM`, `IT`, `S3M`, and more), and a live V4L2 camera viewfinder with digital zoom, flash/torch control, self-timer, and photo gallery.
- **File manager (`xfiles`), music and tracker player (`xmusic`), and camera (`xcamera`)**: keypad directory browser with built-in text viewer, integrated GUI multi-tap text editor (**Edit in Text Editor**, available as an action on any file), and image viewer (plus one-click actions to launch `.gb`/`.gbc` ROMs in the Game Boy emulator, run `.yab` scripts in Yabasic inside the terminal, play audio, preview images, set any image as wallpaper, or install any audio or tracker file as ringtone), a 3-tab music and chiptune tracker module player (`WAV`, `MP3`, `OGG`, `FLAC`, `Opus`, `MOD`, `XM`, `IT`, `S3M`, and more), and a live V4L2 camera viewfinder with digital zoom, flash/torch control, self-timer, and photo gallery.
- **Themes (`xthemes`), wallpapers (`xwallpapers`), and ringtones (`xringtones`)**: installable `.Xresources` color and font themes (`~/.xthemes/*.xth`, 20 preinstalled schemes) with live zero-restart reload across the entire UI and terminal, alongside dedicated managers for desktop wallpapers and call ringtones (supporting both standard audio and tracker modules) with 3-level vibration control.
### Input, terminal, and developer tools
Binary file not shown.
Binary file not shown.
+658 -9
View File
@@ -29,6 +29,7 @@ enum class ViewMode {
PropsView = 4,
InputModal = 5,
ConfirmDelete = 6,
TextEdit = 7,
};
enum class SortMode {
@@ -48,6 +49,7 @@ enum class TextCaseMode {
Title = 1, // Abc
Upper = 2, // ABC
Num = 3, // 123
Sym = 4, // Sym
};
const char* kMultiTapFile[10] = {
@@ -63,6 +65,34 @@ const char* kMultiTapFile[10] = {
"wxyz9" // 9
};
const char* kMultiTapEditorAlpha[11] = {
" 0", // 0
".,:;!?-_1/\"'()", // 1
"abc2", // 2
"def3", // 3
"ghi4", // 4
"jkl5", // 5
"mno6", // 6
"pqrs7", // 7
"tuv8", // 8
"wxyz9", // 9
".,!?+-*/=<>()[]{}\"'_#$%&|\\~^@" // 10 (*)
};
const char* kMultiTapEditorSym[11] = {
" _0", // 0
".,:;!?1", // 1
"\"'`~@2", // 2
"()[]{}<>3", // 3
"+-*/=%4", // 4
"|&^#$5", // 5
"_\\/$@6", // 6
"=<>!~7", // 7
"();,\"8", // 8
"{}[]#9", // 9
"+-*/=<>()[]{}\"'.,:;!?_|&^#$%~@\\" // 10 (*)
};
struct FileItem {
std::string name;
std::string fullPath;
@@ -84,6 +114,48 @@ void popUtf8(std::string& s) {
if (!s.empty()) s.pop_back();
}
size_t prevUtf8Pos(const std::string& s, size_t pos) {
if (pos == 0) return 0;
if (pos > s.size()) pos = s.size();
--pos;
while (pos > 0 && (static_cast<unsigned char>(s[pos]) & 0xC0) == 0x80) {
--pos;
}
return pos;
}
size_t nextUtf8Pos(const std::string& s, size_t pos) {
if (pos >= s.size()) return s.size();
++pos;
while (pos < s.size() && (static_cast<unsigned char>(s[pos]) & 0xC0) == 0x80) {
++pos;
}
return pos;
}
size_t clampUtf8Pos(const std::string& s, size_t pos) {
if (pos >= s.size()) return s.size();
while (pos > 0 && (static_cast<unsigned char>(s[pos]) & 0xC0) == 0x80) {
--pos;
}
return pos;
}
std::string expandTabsForDisplay(const std::string& s) {
std::string out;
out.reserve(s.size());
for (char c : s) {
if (c == '\t') {
out += " ";
} else if (static_cast<unsigned char>(c) < 0x20) {
out.push_back('?');
} else {
out.push_back(c);
}
}
return out;
}
std::string getExtLower(const std::string& name) {
size_t dot = name.rfind('.');
if (dot == std::string::npos || dot + 1 >= name.size()) {
@@ -122,6 +194,10 @@ bool isGameBoyExt(const std::string& ext) {
return ext == "gb" || ext == "gbc";
}
bool isYabasicExt(const std::string& ext) {
return ext == "yab";
}
bool isLikelyTextFile(const std::string& path, off_t size) {
if (size == 0) return true;
if (size > 256 * 1024) return false;
@@ -323,6 +399,21 @@ int runFilesApp(int argc, char** argv) {
std::vector<std::string> textLines;
int textScrollLine = 0;
// Integrated GUI text editor state
std::string editFilePath;
std::string editFileName;
std::vector<std::string> editLines = {""};
int editCursorRow = 0;
size_t editCursorCol = 0;
int editScrollRow = 0;
bool editModified = false;
bool editMenuOpen = false;
int editMenuSel = 0;
TextCaseMode editCaseMode = TextCaseMode::Lower;
int editLastTapDigit = -1;
int editLastTapCycle = 0;
int64_t editLastTapMs = 0;
// Image viewer state
constexpr int kImgCanvasW = 240;
constexpr int kImgCanvasH = 232;
@@ -505,6 +596,86 @@ int runFilesApp(int argc, char** argv) {
mode = ViewMode::TextView;
};
constexpr int kEditVisibleRows = 10;
auto syncEditorScroll = [&]() {
if (editLines.empty()) {
editLines.push_back("");
}
editCursorRow = std::clamp(editCursorRow, 0,
static_cast<int>(editLines.size()) - 1);
editCursorCol =
clampUtf8Pos(editLines[editCursorRow], editCursorCol);
if (editCursorRow < editScrollRow) {
editScrollRow = editCursorRow;
} else if (editCursorRow >= editScrollRow + kEditVisibleRows) {
editScrollRow = editCursorRow - kEditVisibleRows + 1;
}
int maxScroll =
std::max(0, static_cast<int>(editLines.size()) - kEditVisibleRows);
editScrollRow = std::clamp(editScrollRow, 0, maxScroll);
};
auto openTextEditor = [&](const std::string& fullPath,
const std::string& fileName) {
editFilePath = fullPath;
editFileName = fileName;
editLines.clear();
editCursorRow = 0;
editCursorCol = 0;
editScrollRow = 0;
editModified = false;
editMenuOpen = false;
editMenuSel = 0;
editLastTapDigit = -1;
std::ifstream in(fullPath, std::ios::binary);
if (in.is_open()) {
std::string line;
int maxLines = 4000;
while (maxLines-- > 0 && std::getline(in, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
for (char& c : line) {
if (c == '\0') c = ' ';
}
editLines.push_back(line);
}
}
if (editLines.empty()) {
editLines.push_back("");
}
syncEditorScroll();
mode = ViewMode::TextEdit;
};
auto saveEditedFile = [&]() -> bool {
std::ofstream out(editFilePath, std::ios::binary | std::ios::trunc);
if (!out.is_open()) {
statusBanner = "Save failed (read-only)";
statusBannerUntilMs = nowMs() + 2000;
return false;
}
if (!(editLines.size() == 1 && editLines[0].empty())) {
for (size_t i = 0; i < editLines.size(); ++i) {
out.write(editLines[i].data(),
static_cast<std::streamsize>(editLines[i].size()));
out.put('\n');
}
}
out.flush();
if (!out.good()) {
statusBanner = "Write error";
statusBannerUntilMs = nowMs() + 2000;
return false;
}
editModified = false;
statusBanner = "Saved: " + editFileName;
statusBannerUntilMs = nowMs() + 1800;
return true;
};
auto openImageViewer = [&](const FileItem& item) {
viewingFilePath = item.fullPath;
viewingFileName = item.name;
@@ -618,9 +789,13 @@ int runFilesApp(int argc, char** argv) {
actionsList.push_back({15, kIconBell, "Set as Ringtone"});
} else if (isGameBoyExt(ext)) {
actionsList.push_back({1, kIconGamepad, "Play in Game Boy Emulator"});
} else if (isYabasicExt(ext)) {
actionsList.push_back({1, kIconTerm, "Run in Yabasic (Terminal)"});
actionsList.push_back({17, kIconEye, "View Text"});
} else {
actionsList.push_back({1, kIconEye, "Open / View"});
}
actionsList.push_back({16, kIconEdit, "Edit in Text Editor"});
} else {
actionsList.push_back({1, kIconFolderOpen, "Open Folder"});
}
@@ -673,6 +848,14 @@ int runFilesApp(int argc, char** argv) {
}
};
auto runYabasicInTerminal = [&](const std::string& path) {
std::string d = currentDir;
hideWindow();
runAsyncCmd("cd " + shellQuote(d) +
" && exec st -e sh -c 'yabasic " + shellQuote(path) +
"; rc=$?; printf \"\\n[Yabasic exited (%d) - press Enter]\" \"$rc\"; read -r _'");
};
auto openSelectedItem = [&]() {
if (filteredItems.empty()) return;
const FileItem item = filteredItems[selected];
@@ -699,6 +882,9 @@ int runFilesApp(int argc, char** argv) {
hideWindow();
runAsyncArgv("gambatte", item.fullPath.c_str());
return;
} else if (isYabasicExt(ext)) {
runYabasicInTerminal(item.fullPath);
return;
} else if (isMediaExt(ext)) {
hideWindow();
runAsyncArgv("sxmo_open.sh", item.fullPath.c_str());
@@ -845,7 +1031,7 @@ int runFilesApp(int argc, char** argv) {
} else if (isGameBoyExt(ext)) {
ic = kIconGamepad;
icCol = &colYellow;
} else if (it.isExec || ext == "sh") {
} else if (isYabasicExt(ext) || it.isExec || ext == "sh") {
ic = kIconTerm;
icCol = &colOrange;
} else {
@@ -1032,6 +1218,174 @@ int runFilesApp(int argc, char** argv) {
i == 0 ? "No, Keep Item" : "Yes, Delete", 184);
}
drawSoftkeyBar("Select", "Confirm", "Cancel");
} else if (mode == ViewMode::TextEdit) {
syncEditorScroll();
XSetForeground(dpy, gc, pt.spec.bgHeader);
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 32);
XSetForeground(dpy, gc, pt.spec.border);
XDrawLine(dpy, pix, gc, 0, 31, kWinWidth, 31);
drawPango(layoutIcon, editModified ? &colOrange : &colBlue, 5, 0, 32,
kIconEdit);
std::string titleDisp = (editModified ? "*" : "") + editFileName;
drawPango(layoutMain, &colWhite, 25, 0, 32, titleDisp, 114);
const char* mBadge = "[abc]";
if (editCaseMode == TextCaseMode::Title) mBadge = "[Abc]";
else if (editCaseMode == TextCaseMode::Upper) mBadge = "[ABC]";
else if (editCaseMode == TextCaseMode::Num) mBadge = "[123]";
else if (editCaseMode == TextCaseMode::Sym) mBadge = "[Sym]";
std::string rightHdr = std::to_string(editCursorRow + 1) + ":" +
std::to_string(editCursorCol + 1) + " " + mBadge;
pango_layout_set_width(layoutSmall, -1);
pango_layout_set_text(layoutSmall, rightHdr.c_str(),
static_cast<int>(rightHdr.size()));
int rhw = 0, rhh = 0;
pango_layout_get_pixel_size(layoutSmall, &rhw, &rhh);
drawPango(layoutSmall, &colYellow, kWinWidth - rhw - 5, 0, 32, rightHdr);
const int startY = 33;
const int rowH = 21;
const int gutterW = 28;
XSetForeground(dpy, gc, pt.spec.bgAltOdd);
XFillRectangle(dpy, pix, gc, 0, startY, gutterW,
kEditVisibleRows * rowH);
XSetForeground(dpy, gc, pt.spec.divider);
XDrawLine(dpy, pix, gc, gutterW, startY, gutterW,
startY + kEditVisibleRows * rowH);
int64_t tCur = nowMs();
bool tapActive =
(editLastTapDigit != -1 && (tCur - editLastTapMs) <= 850);
for (int i = 0; i < kEditVisibleRows; ++i) {
int idx = editScrollRow + i;
if (idx >= static_cast<int>(editLines.size())) break;
int ry = startY + i * rowH;
bool isCurRow = (idx == editCursorRow);
if (isCurRow) {
XSetForeground(dpy, gc, pt.spec.bgInputFocus);
XFillRectangle(dpy, pix, gc, gutterW + 1, ry,
kWinWidth - gutterW - 1, rowH);
}
std::string lnStr = std::to_string(idx + 1);
drawPango(layoutSmall, isCurRow ? &colYellow : &colMuted, 3, ry, rowH,
lnStr, gutterW - 4);
const std::string& rawLine = editLines[idx];
size_t sliceStart = 0;
if (isCurRow && editCursorCol > 22) {
sliceStart = clampUtf8Pos(rawLine, editCursorCol - 22);
}
std::string rawSlice = rawLine.substr(sliceStart);
std::string dispSlice = expandTabsForDisplay(rawSlice);
drawPango(layoutSmall, isCurRow ? &colWhite : &colLight, gutterW + 4,
ry, rowH, dispSlice, kWinWidth - gutterW - 8);
if (isCurRow && !editMenuOpen) {
size_t curOff =
(editCursorCol >= sliceStart) ? (editCursorCol - sliceStart) : 0;
std::string beforeCur =
expandTabsForDisplay(rawSlice.substr(0, curOff));
pango_layout_set_width(layoutSmall, -1);
pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE);
pango_layout_set_text(layoutSmall, beforeCur.c_str(),
static_cast<int>(beforeCur.size()));
int cx = 0, chh = 0;
pango_layout_get_pixel_size(layoutSmall, &cx, &chh);
int curX = std::min(kWinWidth - 4, gutterW + 4 + cx);
XSetForeground(dpy, gc,
tapActive ? pt.spec.success : pt.spec.accentAlt);
XFillRectangle(dpy, pix, gc, curX, ry + 3, 2, rowH - 6);
}
}
// Bottom candidate / hint bar
const int hintY = kWinHeight - 46;
XSetForeground(dpy, gc, pt.spec.bgAltOdd);
XFillRectangle(dpy, pix, gc, 0, hintY, kWinWidth, 20);
XSetForeground(dpy, gc, pt.spec.divider);
XDrawLine(dpy, pix, gc, 0, hintY, kWinWidth, hintY);
if (tapActive && editLastTapDigit >= 0 && editLastTapDigit <= 10) {
const char* table =
(editCaseMode == TextCaseMode::Sym)
? kMultiTapEditorSym[editLastTapDigit]
: kMultiTapEditorAlpha[editLastTapDigit];
int tlen = static_cast<int>(std::strlen(table));
std::string cand =
(editLastTapDigit == 10)
? "*: "
: (std::to_string(editLastTapDigit) + ": ");
for (int k = 0; k < tlen; ++k) {
char c = table[k];
if (std::isalpha(static_cast<unsigned char>(c)) &&
editCaseMode == TextCaseMode::Upper) {
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
std::string label = (c == ' ') ? "SPC" : std::string(1, c);
if (k == editLastTapCycle) {
cand += "[" + label + "] ";
} else {
cand += label + " ";
}
}
drawPango(layoutSmall, &colYellow, 6, hintY, 20, cand, kWinWidth - 12);
} else {
drawPango(layoutSmall, &colMuted, 6, hintY, 20,
"#:Mode OK:Newline *:Sym Menu:Save", kWinWidth - 12);
}
if (editMenuOpen) {
static const char* kEditMenuLabels[7] = {
"Save File",
"Save & Close",
"Insert New Line",
"Insert Space",
"Duplicate Line",
"Delete Line",
"Close Without Saving"
};
const int mW = 212;
const int mRowH = 26;
const int mH = 28 + 7 * mRowH + 6;
const int mX = (kWinWidth - mW) / 2;
const int mY = 26;
XSetForeground(dpy, gc, pt.spec.bgHeader);
XFillRectangle(dpy, pix, gc, mX, mY, mW, mH);
XSetForeground(dpy, gc, pt.spec.accentAlt);
XDrawRectangle(dpy, pix, gc, mX, mY, mW - 1, mH - 1);
drawPango(layoutMain, &colYellow, mX + 10, mY + 2, 24, "Editor Menu",
mW - 20);
XSetForeground(dpy, gc, pt.spec.divider);
XDrawLine(dpy, pix, gc, mX + 1, mY + 26, mX + mW - 2, mY + 26);
for (int i = 0; i < 7; ++i) {
int ry = mY + 28 + i * mRowH;
bool isSel = (i == editMenuSel);
if (isSel) {
XSetForeground(
dpy, gc,
(i == 6 && editModified) ? pt.spec.dangerBg : pt.spec.selBg);
XFillRectangle(dpy, pix, gc, mX + 2, ry, mW - 4, mRowH - 1);
}
drawPango(layoutSmall, &colWhite, mX + 12, ry, mRowH,
kEditMenuLabels[i], mW - 24);
}
drawSoftkeyBar("Select", "Select", "Back");
} else {
bool docEmpty =
(editLines.size() == 1 && editLines[0].empty() && !editModified);
drawSoftkeyBar("Menu/Save", "Enter", docEmpty ? "Close" : "Del");
}
}
int64_t tNow = nowMs();
@@ -1101,6 +1455,169 @@ int runFilesApp(int argc, char** argv) {
inputValue.push_back(ch);
};
auto insertEditorStr = [&](const std::string& s) {
if (s.empty()) return;
if (editLines.empty()) editLines.push_back("");
editCursorRow = std::clamp(editCursorRow, 0,
static_cast<int>(editLines.size()) - 1);
std::string& line = editLines[editCursorRow];
editCursorCol = clampUtf8Pos(line, editCursorCol);
line.insert(static_cast<size_t>(editCursorCol), s);
editCursorCol += static_cast<int>(s.size());
editModified = true;
syncEditorScroll();
};
auto deleteEditorCharBefore = [&]() {
editLastTapDigit = -1;
if (editLines.empty()) {
editLines.push_back("");
editCursorRow = 0;
editCursorCol = 0;
return;
}
editCursorRow = std::clamp(editCursorRow, 0,
static_cast<int>(editLines.size()) - 1);
std::string& line = editLines[editCursorRow];
editCursorCol = clampUtf8Pos(line, editCursorCol);
if (editCursorCol > 0) {
int prevPos = prevUtf8Pos(line, editCursorCol);
line.erase(static_cast<size_t>(prevPos),
static_cast<size_t>(editCursorCol - prevPos));
editCursorCol = prevPos;
editModified = true;
} else if (editCursorRow > 0) {
int prevLen = static_cast<int>(editLines[editCursorRow - 1].size());
editLines[editCursorRow - 1] += line;
editLines.erase(editLines.begin() + editCursorRow);
editCursorRow--;
editCursorCol = prevLen;
editModified = true;
}
syncEditorScroll();
};
auto insertEditorNewline = [&]() {
editLastTapDigit = -1;
if (editLines.empty()) editLines.push_back("");
editCursorRow = std::clamp(editCursorRow, 0,
static_cast<int>(editLines.size()) - 1);
std::string& line = editLines[editCursorRow];
editCursorCol = clampUtf8Pos(line, editCursorCol);
std::string tail = line.substr(static_cast<size_t>(editCursorCol));
line.erase(static_cast<size_t>(editCursorCol));
editLines.insert(editLines.begin() + editCursorRow + 1, tail);
editCursorRow++;
editCursorCol = 0;
editModified = true;
syncEditorScroll();
};
auto commitEditorTap = [&](int keyIdx) {
if (keyIdx < 0 || keyIdx > 10) return;
if (editLines.empty()) editLines.push_back("");
editCursorRow = std::clamp(editCursorRow, 0,
static_cast<int>(editLines.size()) - 1);
std::string& line = editLines[editCursorRow];
editCursorCol = clampUtf8Pos(line, editCursorCol);
if (editCaseMode == TextCaseMode::Num) {
editLastTapDigit = -1;
char c = (keyIdx == 10) ? '*' : static_cast<char>('0' + keyIdx);
insertEditorStr(std::string(1, c));
return;
}
int64_t t = nowMs();
const char* chars = (editCaseMode == TextCaseMode::Sym)
? kMultiTapEditorSym[keyIdx]
: kMultiTapEditorAlpha[keyIdx];
int len = static_cast<int>(std::strlen(chars));
if (len <= 0) return;
bool replacePrev =
(keyIdx == editLastTapDigit && (t - editLastTapMs) <= 850 &&
editCursorCol > 0);
if (replacePrev) {
int prevPos = prevUtf8Pos(line, editCursorCol);
line.erase(static_cast<size_t>(prevPos),
static_cast<size_t>(editCursorCol - prevPos));
editCursorCol = prevPos;
editLastTapCycle = (editLastTapCycle + 1) % len;
} else {
editLastTapDigit = keyIdx;
editLastTapCycle = 0;
}
editLastTapMs = t;
char ch = chars[editLastTapCycle];
if (std::isalpha(static_cast<unsigned char>(ch))) {
if (editCaseMode == TextCaseMode::Upper) {
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
} else if (editCaseMode == TextCaseMode::Title) {
bool atWordStart = (editCursorCol == 0);
if (!atWordStart && editCursorCol > 0) {
unsigned char prevCh =
static_cast<unsigned char>(line[editCursorCol - 1]);
atWordStart = !std::isalnum(prevCh) && prevCh != '_';
}
if (atWordStart) {
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
}
}
}
line.insert(static_cast<size_t>(editCursorCol), 1, ch);
editCursorCol += 1;
editModified = true;
syncEditorScroll();
};
auto executeEditorMenuAction = [&](int menuIdx) {
editMenuOpen = false;
editLastTapDigit = -1;
if (menuIdx == 0) {
saveEditedFile();
} else if (menuIdx == 1) {
if (saveEditedFile()) {
loadDirectory(currentDir, editFileName);
mode = ViewMode::Browser;
}
} else if (menuIdx == 2) {
insertEditorNewline();
} else if (menuIdx == 3) {
insertEditorStr(" ");
} else if (menuIdx == 4) {
if (editLines.empty()) editLines.push_back("");
editCursorRow = std::clamp(editCursorRow, 0,
static_cast<int>(editLines.size()) - 1);
std::string copyLine = editLines[editCursorRow];
editLines.insert(editLines.begin() + editCursorRow + 1, copyLine);
editCursorRow++;
editCursorCol = clampUtf8Pos(editLines[editCursorRow], editCursorCol);
editModified = true;
syncEditorScroll();
} else if (menuIdx == 5) {
if (editLines.size() <= 1) {
editLines = {""};
editCursorRow = 0;
editCursorCol = 0;
} else {
editCursorRow = std::clamp(editCursorRow, 0,
static_cast<int>(editLines.size()) - 1);
editLines.erase(editLines.begin() + editCursorRow);
if (editCursorRow >= static_cast<int>(editLines.size())) {
editCursorRow = static_cast<int>(editLines.size()) - 1;
}
editCursorCol = clampUtf8Pos(editLines[editCursorRow], editCursorCol);
}
editModified = true;
syncEditorScroll();
} else if (menuIdx == 6) {
loadDirectory(currentDir, editFileName);
mode = ViewMode::Browser;
}
};
auto executeAction = [&](int actId) {
if (actId == 1) {
mode = ViewMode::Browser;
@@ -1108,7 +1625,7 @@ int runFilesApp(int argc, char** argv) {
} else if (actId == 2 && !filteredItems.empty()) {
std::string p = filteredItems[selected].fullPath;
hideWindow();
runAsyncCmd("st -e nano \"" + p + "\"");
runAsyncCmd("st -e nano " + shellQuote(p));
} else if (actId == 3) {
inputAction = InputAction::NewFolder;
inputTitle = "New Folder";
@@ -1173,7 +1690,7 @@ int runFilesApp(int argc, char** argv) {
} else if (actId == 12) {
std::string d = currentDir;
hideWindow();
runAsyncCmd("cd \"" + d + "\" && exec st");
runAsyncCmd("cd " + shellQuote(d) + " && exec st");
} else if (actId == 13 && !filteredItems.empty()) {
confirmSelected = 0;
mode = ViewMode::ConfirmDelete;
@@ -1185,6 +1702,11 @@ int runFilesApp(int argc, char** argv) {
installAsRingtone(filteredItems[selected].fullPath,
filteredItems[selected].name);
mode = ViewMode::Browser;
} else if (actId == 16 && !filteredItems.empty()) {
openTextEditor(filteredItems[selected].fullPath,
filteredItems[selected].name);
} else if (actId == 17 && !filteredItems.empty()) {
openTextViewer(filteredItems[selected]);
}
};
@@ -1226,13 +1748,33 @@ int runFilesApp(int argc, char** argv) {
}
} else if (mode == ViewMode::TextView) {
if (isLeft) {
std::string p = viewingFilePath;
hideWindow();
runAsyncCmd("st -e nano \"" + p + "\"");
return;
openTextEditor(viewingFilePath, viewingFileName);
} else {
mode = ViewMode::Browser;
}
} else if (mode == ViewMode::TextEdit) {
if (editMenuOpen) {
if (isLeft) {
executeEditorMenuAction(editMenuSel);
} else {
editMenuOpen = false;
}
} else {
if (isLeft) {
editLastTapDigit = -1;
editMenuOpen = true;
editMenuSel = 0;
} else {
bool isEmptyDoc =
(editLines.size() == 1 && editLines[0].empty());
if (isEmptyDoc && !editModified) {
loadDirectory(currentDir, editFileName);
mode = ViewMode::Browser;
} else {
deleteEditorCharBefore();
}
}
}
} else if (mode == ViewMode::ImageView) {
if (isLeft) {
installAsWallpaper(viewingFilePath, viewingFileName);
@@ -1253,15 +1795,21 @@ int runFilesApp(int argc, char** argv) {
: (currentDir + "/" + name);
if (inputAction == InputAction::NewFolder) {
::mkdir(target.c_str(), 0755);
loadDirectory(currentDir, name);
mode = ViewMode::Browser;
} else if (inputAction == InputAction::NewFile) {
int fd = ::open(target.c_str(), O_WRONLY | O_CREAT, 0644);
if (fd >= 0) ::close(fd);
loadDirectory(currentDir, name);
openTextEditor(target, name);
} else if (inputAction == InputAction::Rename) {
::rename(inputTargetPath.c_str(), target.c_str());
}
loadDirectory(currentDir, name);
}
mode = ViewMode::Browser;
}
} else {
mode = ViewMode::Browser;
}
} else {
if (!inputValue.empty()) {
popUtf8(inputValue);
@@ -1461,6 +2009,88 @@ int runFilesApp(int argc, char** argv) {
textScrollLine + 10);
drawUI();
}
} else if (mode == ViewMode::TextEdit) {
if (editMenuOpen) {
if (sym == XK_Up || sym == XK_KP_Up) {
editMenuSel = (editMenuSel - 1 + 7) % 7;
drawUI();
} else if (sym == XK_Down || sym == XK_KP_Down) {
editMenuSel = (editMenuSel + 1) % 7;
drawUI();
} else if (sym == XK_Return || sym == XK_KP_Enter) {
executeEditorMenuAction(editMenuSel);
drawUI();
}
} else {
if (sym == XK_Up || sym == XK_KP_Up) {
editLastTapDigit = -1;
if (editCursorRow > 0) {
editCursorRow--;
editCursorCol =
clampUtf8Pos(editLines[editCursorRow], editCursorCol);
syncEditorScroll();
drawUI();
}
} else if (sym == XK_Down || sym == XK_KP_Down) {
editLastTapDigit = -1;
if (editCursorRow + 1 < static_cast<int>(editLines.size())) {
editCursorRow++;
editCursorCol =
clampUtf8Pos(editLines[editCursorRow], editCursorCol);
syncEditorScroll();
drawUI();
}
} else if (sym == XK_Left || sym == XK_KP_Left) {
editLastTapDigit = -1;
if (editCursorCol > 0) {
editCursorCol =
prevUtf8Pos(editLines[editCursorRow], editCursorCol);
} else if (editCursorRow > 0) {
editCursorRow--;
editCursorCol =
static_cast<int>(editLines[editCursorRow].size());
}
syncEditorScroll();
drawUI();
} else if (sym == XK_Right || sym == XK_KP_Right) {
if (editLastTapDigit != -1 &&
(nowMs() - editLastTapMs) <= 850) {
editLastTapDigit = -1;
drawUI();
} else {
editLastTapDigit = -1;
int lineLen =
static_cast<int>(editLines[editCursorRow].size());
if (editCursorCol < lineLen) {
editCursorCol =
nextUtf8Pos(editLines[editCursorRow], editCursorCol);
} else if (editCursorRow + 1 <
static_cast<int>(editLines.size())) {
editCursorRow++;
editCursorCol = 0;
}
syncEditorScroll();
drawUI();
}
} else if (sym == XK_Return || sym == XK_KP_Enter) {
insertEditorNewline();
drawUI();
} else {
char d = mapDialKey(sym, kc, ev.xkey.state);
if (d == '#') {
editLastTapDigit = -1;
editCaseMode = static_cast<TextCaseMode>(
(static_cast<int>(editCaseMode) + 1) % 5);
drawUI();
} else if (d == '*') {
commitEditorTap(10);
drawUI();
} else if (d >= '0' && d <= '9') {
commitEditorTap(d - '0');
drawUI();
}
}
}
} else if (mode == ViewMode::ImageView) {
char d = mapDialKey(sym, kc, ev.xkey.state);
if (d == '*') {
@@ -1524,11 +2154,30 @@ int runFilesApp(int argc, char** argv) {
handleSoftkeyAction(isLeft);
}
}
if (visible && mode == ViewMode::TextEdit && editLastTapDigit != -1 &&
(tNow - editLastTapMs) > 850) {
editLastTapDigit = -1;
drawUI();
}
int timeoutMs = -1;
if (visible && softkeyPending != 0) {
timeoutMs = std::max(1, static_cast<int>(softkeyDeadlineMs - tNow));
}
if (visible && mode == ViewMode::TextEdit && editLastTapDigit != -1) {
int tapRem = std::max(1, static_cast<int>(851 - (tNow - editLastTapMs)));
timeoutMs = (timeoutMs < 0) ? tapRem : std::min(timeoutMs, tapRem);
}
if (visible && !statusBanner.empty() && statusBannerUntilMs > 0) {
if (tNow >= statusBannerUntilMs) {
statusBanner.clear();
statusBannerUntilMs = 0;
drawUI();
} else {
int banRem = std::max(1, static_cast<int>(statusBannerUntilMs - tNow));
timeoutMs = (timeoutMs < 0) ? banRem : std::min(timeoutMs, banRem);
}
}
struct pollfd pfds[2];
int nfds = 0;