feat(xfiles): add Yabasic .yab terminal execution and integrated GUI text editor
This commit is contained in:
+3
-1
@@ -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`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
@@ -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;
|
||||
}
|
||||
loadDirectory(currentDir, name);
|
||||
} else {
|
||||
mode = ViewMode::Browser;
|
||||
}
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user