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`.
|
- 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.
|
- 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 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.
|
- 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`)**:
|
8. **File manager (`files.so` / `xfiles`)**:
|
||||||
- **Open**: select **Files** from the System menu or press `Mod4+d`.
|
- **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 `#`.
|
- **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.
|
- **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.
|
- **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.
|
- **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`)**:
|
9. **Camera (`camera.so` / `xcamera`)**:
|
||||||
- **Open**: select **Camera** from the System menu or run `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.
|
- **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.
|
- **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.
|
- **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.
|
- **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
|
### Input, terminal, and developer tools
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -29,6 +29,7 @@ enum class ViewMode {
|
|||||||
PropsView = 4,
|
PropsView = 4,
|
||||||
InputModal = 5,
|
InputModal = 5,
|
||||||
ConfirmDelete = 6,
|
ConfirmDelete = 6,
|
||||||
|
TextEdit = 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class SortMode {
|
enum class SortMode {
|
||||||
@@ -48,6 +49,7 @@ enum class TextCaseMode {
|
|||||||
Title = 1, // Abc
|
Title = 1, // Abc
|
||||||
Upper = 2, // ABC
|
Upper = 2, // ABC
|
||||||
Num = 3, // 123
|
Num = 3, // 123
|
||||||
|
Sym = 4, // Sym
|
||||||
};
|
};
|
||||||
|
|
||||||
const char* kMultiTapFile[10] = {
|
const char* kMultiTapFile[10] = {
|
||||||
@@ -63,6 +65,34 @@ const char* kMultiTapFile[10] = {
|
|||||||
"wxyz9" // 9
|
"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 {
|
struct FileItem {
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string fullPath;
|
std::string fullPath;
|
||||||
@@ -84,6 +114,48 @@ void popUtf8(std::string& s) {
|
|||||||
if (!s.empty()) s.pop_back();
|
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) {
|
std::string getExtLower(const std::string& name) {
|
||||||
size_t dot = name.rfind('.');
|
size_t dot = name.rfind('.');
|
||||||
if (dot == std::string::npos || dot + 1 >= name.size()) {
|
if (dot == std::string::npos || dot + 1 >= name.size()) {
|
||||||
@@ -122,6 +194,10 @@ bool isGameBoyExt(const std::string& ext) {
|
|||||||
return ext == "gb" || ext == "gbc";
|
return ext == "gb" || ext == "gbc";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isYabasicExt(const std::string& ext) {
|
||||||
|
return ext == "yab";
|
||||||
|
}
|
||||||
|
|
||||||
bool isLikelyTextFile(const std::string& path, off_t size) {
|
bool isLikelyTextFile(const std::string& path, off_t size) {
|
||||||
if (size == 0) return true;
|
if (size == 0) return true;
|
||||||
if (size > 256 * 1024) return false;
|
if (size > 256 * 1024) return false;
|
||||||
@@ -323,6 +399,21 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
std::vector<std::string> textLines;
|
std::vector<std::string> textLines;
|
||||||
int textScrollLine = 0;
|
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
|
// Image viewer state
|
||||||
constexpr int kImgCanvasW = 240;
|
constexpr int kImgCanvasW = 240;
|
||||||
constexpr int kImgCanvasH = 232;
|
constexpr int kImgCanvasH = 232;
|
||||||
@@ -505,6 +596,86 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
mode = ViewMode::TextView;
|
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) {
|
auto openImageViewer = [&](const FileItem& item) {
|
||||||
viewingFilePath = item.fullPath;
|
viewingFilePath = item.fullPath;
|
||||||
viewingFileName = item.name;
|
viewingFileName = item.name;
|
||||||
@@ -618,9 +789,13 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
actionsList.push_back({15, kIconBell, "Set as Ringtone"});
|
actionsList.push_back({15, kIconBell, "Set as Ringtone"});
|
||||||
} else if (isGameBoyExt(ext)) {
|
} else if (isGameBoyExt(ext)) {
|
||||||
actionsList.push_back({1, kIconGamepad, "Play in Game Boy Emulator"});
|
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 {
|
} else {
|
||||||
actionsList.push_back({1, kIconEye, "Open / View"});
|
actionsList.push_back({1, kIconEye, "Open / View"});
|
||||||
}
|
}
|
||||||
|
actionsList.push_back({16, kIconEdit, "Edit in Text Editor"});
|
||||||
} else {
|
} else {
|
||||||
actionsList.push_back({1, kIconFolderOpen, "Open Folder"});
|
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 = [&]() {
|
auto openSelectedItem = [&]() {
|
||||||
if (filteredItems.empty()) return;
|
if (filteredItems.empty()) return;
|
||||||
const FileItem item = filteredItems[selected];
|
const FileItem item = filteredItems[selected];
|
||||||
@@ -699,6 +882,9 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
hideWindow();
|
hideWindow();
|
||||||
runAsyncArgv("gambatte", item.fullPath.c_str());
|
runAsyncArgv("gambatte", item.fullPath.c_str());
|
||||||
return;
|
return;
|
||||||
|
} else if (isYabasicExt(ext)) {
|
||||||
|
runYabasicInTerminal(item.fullPath);
|
||||||
|
return;
|
||||||
} else if (isMediaExt(ext)) {
|
} else if (isMediaExt(ext)) {
|
||||||
hideWindow();
|
hideWindow();
|
||||||
runAsyncArgv("sxmo_open.sh", item.fullPath.c_str());
|
runAsyncArgv("sxmo_open.sh", item.fullPath.c_str());
|
||||||
@@ -845,7 +1031,7 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
} else if (isGameBoyExt(ext)) {
|
} else if (isGameBoyExt(ext)) {
|
||||||
ic = kIconGamepad;
|
ic = kIconGamepad;
|
||||||
icCol = &colYellow;
|
icCol = &colYellow;
|
||||||
} else if (it.isExec || ext == "sh") {
|
} else if (isYabasicExt(ext) || it.isExec || ext == "sh") {
|
||||||
ic = kIconTerm;
|
ic = kIconTerm;
|
||||||
icCol = &colOrange;
|
icCol = &colOrange;
|
||||||
} else {
|
} else {
|
||||||
@@ -1032,6 +1218,174 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
i == 0 ? "No, Keep Item" : "Yes, Delete", 184);
|
i == 0 ? "No, Keep Item" : "Yes, Delete", 184);
|
||||||
}
|
}
|
||||||
drawSoftkeyBar("Select", "Confirm", "Cancel");
|
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();
|
int64_t tNow = nowMs();
|
||||||
@@ -1101,6 +1455,169 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
inputValue.push_back(ch);
|
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) {
|
auto executeAction = [&](int actId) {
|
||||||
if (actId == 1) {
|
if (actId == 1) {
|
||||||
mode = ViewMode::Browser;
|
mode = ViewMode::Browser;
|
||||||
@@ -1108,7 +1625,7 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
} else if (actId == 2 && !filteredItems.empty()) {
|
} else if (actId == 2 && !filteredItems.empty()) {
|
||||||
std::string p = filteredItems[selected].fullPath;
|
std::string p = filteredItems[selected].fullPath;
|
||||||
hideWindow();
|
hideWindow();
|
||||||
runAsyncCmd("st -e nano \"" + p + "\"");
|
runAsyncCmd("st -e nano " + shellQuote(p));
|
||||||
} else if (actId == 3) {
|
} else if (actId == 3) {
|
||||||
inputAction = InputAction::NewFolder;
|
inputAction = InputAction::NewFolder;
|
||||||
inputTitle = "New Folder";
|
inputTitle = "New Folder";
|
||||||
@@ -1173,7 +1690,7 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
} else if (actId == 12) {
|
} else if (actId == 12) {
|
||||||
std::string d = currentDir;
|
std::string d = currentDir;
|
||||||
hideWindow();
|
hideWindow();
|
||||||
runAsyncCmd("cd \"" + d + "\" && exec st");
|
runAsyncCmd("cd " + shellQuote(d) + " && exec st");
|
||||||
} else if (actId == 13 && !filteredItems.empty()) {
|
} else if (actId == 13 && !filteredItems.empty()) {
|
||||||
confirmSelected = 0;
|
confirmSelected = 0;
|
||||||
mode = ViewMode::ConfirmDelete;
|
mode = ViewMode::ConfirmDelete;
|
||||||
@@ -1185,6 +1702,11 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
installAsRingtone(filteredItems[selected].fullPath,
|
installAsRingtone(filteredItems[selected].fullPath,
|
||||||
filteredItems[selected].name);
|
filteredItems[selected].name);
|
||||||
mode = ViewMode::Browser;
|
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) {
|
} else if (mode == ViewMode::TextView) {
|
||||||
if (isLeft) {
|
if (isLeft) {
|
||||||
std::string p = viewingFilePath;
|
openTextEditor(viewingFilePath, viewingFileName);
|
||||||
hideWindow();
|
|
||||||
runAsyncCmd("st -e nano \"" + p + "\"");
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
mode = ViewMode::Browser;
|
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) {
|
} else if (mode == ViewMode::ImageView) {
|
||||||
if (isLeft) {
|
if (isLeft) {
|
||||||
installAsWallpaper(viewingFilePath, viewingFileName);
|
installAsWallpaper(viewingFilePath, viewingFileName);
|
||||||
@@ -1253,15 +1795,21 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
: (currentDir + "/" + name);
|
: (currentDir + "/" + name);
|
||||||
if (inputAction == InputAction::NewFolder) {
|
if (inputAction == InputAction::NewFolder) {
|
||||||
::mkdir(target.c_str(), 0755);
|
::mkdir(target.c_str(), 0755);
|
||||||
|
loadDirectory(currentDir, name);
|
||||||
|
mode = ViewMode::Browser;
|
||||||
} else if (inputAction == InputAction::NewFile) {
|
} else if (inputAction == InputAction::NewFile) {
|
||||||
int fd = ::open(target.c_str(), O_WRONLY | O_CREAT, 0644);
|
int fd = ::open(target.c_str(), O_WRONLY | O_CREAT, 0644);
|
||||||
if (fd >= 0) ::close(fd);
|
if (fd >= 0) ::close(fd);
|
||||||
|
loadDirectory(currentDir, name);
|
||||||
|
openTextEditor(target, name);
|
||||||
} else if (inputAction == InputAction::Rename) {
|
} else if (inputAction == InputAction::Rename) {
|
||||||
::rename(inputTargetPath.c_str(), target.c_str());
|
::rename(inputTargetPath.c_str(), target.c_str());
|
||||||
}
|
|
||||||
loadDirectory(currentDir, name);
|
loadDirectory(currentDir, name);
|
||||||
}
|
|
||||||
mode = ViewMode::Browser;
|
mode = ViewMode::Browser;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mode = ViewMode::Browser;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!inputValue.empty()) {
|
if (!inputValue.empty()) {
|
||||||
popUtf8(inputValue);
|
popUtf8(inputValue);
|
||||||
@@ -1461,6 +2009,88 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
textScrollLine + 10);
|
textScrollLine + 10);
|
||||||
drawUI();
|
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) {
|
} else if (mode == ViewMode::ImageView) {
|
||||||
char d = mapDialKey(sym, kc, ev.xkey.state);
|
char d = mapDialKey(sym, kc, ev.xkey.state);
|
||||||
if (d == '*') {
|
if (d == '*') {
|
||||||
@@ -1524,11 +2154,30 @@ int runFilesApp(int argc, char** argv) {
|
|||||||
handleSoftkeyAction(isLeft);
|
handleSoftkeyAction(isLeft);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (visible && mode == ViewMode::TextEdit && editLastTapDigit != -1 &&
|
||||||
|
(tNow - editLastTapMs) > 850) {
|
||||||
|
editLastTapDigit = -1;
|
||||||
|
drawUI();
|
||||||
|
}
|
||||||
|
|
||||||
int timeoutMs = -1;
|
int timeoutMs = -1;
|
||||||
if (visible && softkeyPending != 0) {
|
if (visible && softkeyPending != 0) {
|
||||||
timeoutMs = std::max(1, static_cast<int>(softkeyDeadlineMs - tNow));
|
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];
|
struct pollfd pfds[2];
|
||||||
int nfds = 0;
|
int nfds = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user