Files
tactility_apps/Apps/Brainfuck/main/Source/Brainfuck.cpp
T
Shadowtrance dbf850c434 Fixes and new apps (#30)
- M5 Unit Modules library + M5 Unit Test app
- Minor fixes for TodoList, TwoEleven, Snake, Brainfuck and Breakout
- Fixed SerialConsole to use the uart controller as it was broken in one of the many updates
- Fixed keyboard input in TwoEleven, Breakout, Magic8Ball and Snake
- Bluetooth Media Keys app, supports pressing physical keys to trigger the corresponding buttonmatrix button
- Epub Reader app
2026-06-07 15:56:32 +02:00

465 lines
16 KiB
C++

#include "Brainfuck.h"
#include <tt_app.h>
#include <tt_lvgl_toolbar.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
/* ── Built-in examples ────────────────────────────────────────────── */
struct BfExample {
const char* name;
const char* code;
};
static const BfExample examples[] = {
{
"Hello World (built-in)",
"++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]"
">>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."
},
{
"Fibonacci (built-in)",
">++++++++++>+>+[[+++++[>++++++++<-]>.<++++++[>--------<-]+<<<"
"]>.>>[[-]<[>+<-]>>[<<+>+>-]<[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-"
"[>+<-[>+<-[>+<-[>[-]>+>+<<<-[>+<-]]]]]]]]]]]+>>>]<<<]"
},
{
"Alphabet (built-in)",
"+++++[>+++++++++++++<-]++++++++++++++++++++++++++[>.+<-]"
},
{
"Beer (built-in)",
">++++++++++[<++++++++++>-]<->>>>>+++[>+++>+++<<-]<<<<+<[>[>+"
">+<<-]>>[-<<+>>]++++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]>>"
">>>>[[-]<++++++++++<->>]<-[>+>+<<-]>[<+>-]+>[[-]<->]<<<<<<<<"
"<->>]<[>+>+<<-]>>[-<<+>>]+>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<"
"<[>>+>+<<<-]>>>[-<<<+>>>]++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<"
"<[>+<[-]]<[>>+<<[-]]>>[<<+>>[-]]<<<[>>+>+<<<-]>>>[-<<<+>>>]+"
"+++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[>+<[-]]<[>>+<<[-]]>>[<"
"<+>>[-]]<<[[-]>>>++++++++[>>++++++<<-]>[<++++++++[>++++++<-]"
">.<++++++++[>------<-]>[<<+>>-]]>.<<++++++++[>>------<<-]<[-"
">>+<<]<++++++++[<++++>-]<.>+++++++[>+++++++++<-]>+++.<+++++["
">+++++++++<-]>.+++++..--------.-------.++++++++++++++>>[>>>+"
">+<<<<-]>>>>[-<<<<+>>>>]>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<<<"
"[>>>+>+<<<<-]>>>>[-<<<<+>>>>]+>+<[-<->]<[[-]>>-<<]>>[[-]<<+>"
">]<<<[>>+<<[-]]>[>+<[-]]++>>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<"
"+<[[-]>-<]>[<<<<<<<.>>>>>>>[-]]<<<<<<<<<.>>----.---------.<<"
".>>----.+++..+++++++++++++.[-]<<[-]]<[>+>+<<-]>>[-<<+>>]+>+<"
"[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<<[>>+>+<<<-]>>>[-<<<+>>>]++++"
">+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[>+<[-]]<[>>+<<[-]]>>[<<+>"
">[-]]<<[[-]>++++++++[<++++>-]<.>++++++++++[>+++++++++++<-]>+"
".-.<<.>>++++++.------------.---.<<.>++++++[>+++<-]>.<++++++["
">----<-]>++.+++++++++++..[-]<<[-]++++++++++.[-]]<[>+>+<<-]>>"
"[-<<+>>]+++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]++++++++++."
">+++++++++[>+++++++++<-]>+++.+++++++++++++.++++++++++.------"
".<++++++++[>>++++<<-]>>.<++++++++++.-.---------.>.<-.+++++++"
"++++.++++++++.---------.>.<-------------.+++++++++++++.-----"
"-----.>.<++++++++++++.---------------.<+++[>++++++<-]>..>.<-"
"---------.+++++++++++.>.<<+++[>------<-]>-.+++++++++++++++++"
".---.++++++.-------.----------.[-]>[-]<<<.[-]]<[>+>+<<-]>>[-"
"<<+>>]++++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]++++++++++.["
"-]<[-]>]<+<]"
},
};
static constexpr int NUM_EXAMPLES = sizeof(examples) / sizeof(examples[0]);
/* ── App handle for user data path ────────────────────────────────── */
static AppHandle s_appHandle = nullptr;
static bool getScriptDir(char* buf, size_t bufSize) {
if (!s_appHandle) return false;
size_t size = bufSize;
tt_app_get_user_data_path(s_appHandle, buf, &size);
if (size == 0) return false;
for (char* p = buf + 1; *p; ++p) {
if (*p == '/') { *p = '\0'; mkdir(buf, 0755); *p = '/'; }
}
mkdir(buf, 0755);
return true;
}
static char** scriptPaths = nullptr;
static int scriptCount = 0;
static int ciStrcmp(const char* a, const char* b) {
while (*a && *b) {
int ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
int cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
if (ca != cb) return ca - cb;
a++; b++;
}
return (unsigned char)*a - (unsigned char)*b;
}
static void freeScriptPaths() {
for (int i = 0; i < scriptCount; i++) {
free(scriptPaths[i]);
}
free(scriptPaths);
scriptPaths = nullptr;
scriptCount = 0;
}
/* File-scope instance pointer for index-based and path-based callbacks */
static Brainfuck* g_instance = nullptr;
/* ── VM Logic ─────────────────────────────────────────────────────── */
void Brainfuck::bfInit() {
memset(&vm, 0, sizeof(BfVM));
}
static int bfFindBracket(const char* code, int pos, int dir) {
int depth = 1;
while (depth > 0) {
pos += dir;
if (pos < 0 || code[pos] == '\0') return -1;
if (code[pos] == '[') depth += dir;
if (code[pos] == ']') depth -= dir;
}
return pos;
}
void Brainfuck::bfRun(const char* code) {
int len = strlen(code);
while (vm.pc < len && !vm.error) {
vm.cycles++;
if (vm.cycles > MAX_CYCLES) {
vm.error = true;
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Halted after %d cycles (infinite loop?)", MAX_CYCLES);
return;
}
char c = code[vm.pc];
switch (c) {
case '>':
vm.ptr++;
if (vm.ptr >= TAPE_SIZE) {
vm.error = true;
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Pointer overflow (>%d)", TAPE_SIZE);
return;
}
break;
case '<':
vm.ptr--;
if (vm.ptr < 0) {
vm.error = true;
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Pointer underflow (<0)");
return;
}
break;
case '+': vm.tape[vm.ptr]++; break;
case '-': vm.tape[vm.ptr]--; break;
case '.':
if (vm.outLen < MAX_OUTPUT - 1) {
vm.output[vm.outLen++] = (char)vm.tape[vm.ptr];
vm.output[vm.outLen] = '\0';
}
break;
case ',': vm.tape[vm.ptr] = 0; break;
case '[':
if (vm.tape[vm.ptr] == 0) {
int target = bfFindBracket(code, vm.pc, 1);
if (target < 0) {
vm.error = true;
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Unmatched '[' at pos %d", vm.pc);
return;
}
vm.pc = target;
}
break;
case ']':
if (vm.tape[vm.ptr] != 0) {
int target = bfFindBracket(code, vm.pc, -1);
if (target < 0) {
vm.error = true;
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Unmatched ']' at pos %d", vm.pc);
return;
}
vm.pc = target;
}
break;
}
vm.pc++;
}
}
void Brainfuck::runCode(const char* code) {
if (!outputTa) return;
bfInit();
bfRun(code);
constexpr int resultSize = MAX_OUTPUT + 128;
char* result = (char*)malloc(resultSize);
if (!result) {
lv_textarea_set_text(outputTa, "Out of memory");
return;
}
int pos = 0;
int remaining;
remaining = resultSize - pos;
if (remaining > 0) {
int n = snprintf(result + pos, remaining, "> RUN (%d chars)\n", (int)strlen(code));
pos += (n < remaining) ? n : (remaining - 1);
}
if (vm.outLen > 0) {
remaining = resultSize - pos;
if (remaining > 0) {
int n = snprintf(result + pos, remaining, "%s\n", vm.output);
pos += (n < remaining) ? n : (remaining - 1);
}
}
if (vm.error) {
remaining = resultSize - pos;
if (remaining > 0) {
int n = snprintf(result + pos, remaining, "ERROR: %s\n", vm.errorMsg);
pos += (n < remaining) ? n : (remaining - 1);
}
} else {
remaining = resultSize - pos;
if (remaining > 0) {
int n = snprintf(result + pos, remaining, "OK (%d cycles)\n", vm.cycles);
pos += (n < remaining) ? n : (remaining - 1);
}
}
lv_textarea_set_text(outputTa, result);
lv_obj_scroll_to_y(outputTa, LV_COORD_MAX, LV_ANIM_ON);
free(result);
}
/* ── View management ──────────────────────────────────────────────── */
void Brainfuck::showMainView() {
state = BfState::Main;
if (examplesList) lv_obj_add_flag(examplesList, LV_OBJ_FLAG_HIDDEN);
if (outputTa) lv_obj_remove_flag(outputTa, LV_OBJ_FLAG_HIDDEN);
if (inputRow) lv_obj_remove_flag(inputRow, LV_OBJ_FLAG_HIDDEN);
if (clrBtn) lv_obj_remove_flag(clrBtn, LV_OBJ_FLAG_HIDDEN);
}
void Brainfuck::showExamplesView() {
state = BfState::Examples;
if (outputTa) lv_obj_add_flag(outputTa, LV_OBJ_FLAG_HIDDEN);
if (inputRow) lv_obj_add_flag(inputRow, LV_OBJ_FLAG_HIDDEN);
if (examplesList) lv_obj_remove_flag(examplesList, LV_OBJ_FLAG_HIDDEN);
if (clrBtn) lv_obj_add_flag(clrBtn, LV_OBJ_FLAG_HIDDEN);
}
/* ── Callbacks ────────────────────────────────────────────────────── */
void Brainfuck::onRunClicked(lv_event_t* e) {
if (!g_instance || !g_instance->inputTa) return;
const char* code = lv_textarea_get_text(g_instance->inputTa);
if (code && code[0]) {
g_instance->runCode(code);
}
}
void Brainfuck::onClearClicked(lv_event_t* e) {
if (!g_instance) return;
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "");
if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, "");
}
void Brainfuck::onExamplesClicked(lv_event_t* e) {
if (!g_instance) return;
if (g_instance->state == BfState::Examples) {
g_instance->showMainView();
} else {
g_instance->showExamplesView();
}
}
void Brainfuck::onExampleSelected(lv_event_t* e) {
if (!g_instance) return;
int idx = (int)(intptr_t)lv_event_get_user_data(e);
if (idx >= 0 && idx < NUM_EXAMPLES) {
if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, examples[idx].code);
g_instance->showMainView();
g_instance->runCode(examples[idx].code);
}
}
void Brainfuck::onFileSelected(lv_event_t* e) {
if (!g_instance) return;
const char* path = (const char*)lv_event_get_user_data(e);
FILE* f = fopen(path, "rb");
if (!f) {
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "Cannot open file");
g_instance->showMainView();
return;
}
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
if (fsize <= 0 || fsize > 32768) {
fclose(f);
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "File too large or empty");
g_instance->showMainView();
return;
}
fseek(f, 0, SEEK_SET);
char* buf = (char*)malloc(fsize + 1);
if (!buf) {
fclose(f);
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "Out of memory");
g_instance->showMainView();
return;
}
size_t bytesRead = fread(buf, 1, fsize, f);
fclose(f);
buf[bytesRead] = '\0';
if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, buf);
g_instance->showMainView();
g_instance->runCode(buf);
free(buf);
}
void Brainfuck::onInputReady(lv_event_t* e) {
onRunClicked(e);
}
/* ── Script list building ─────────────────────────────────────────── */
void Brainfuck::buildScriptList(lv_obj_t* list) {
freeScriptPaths();
for (int i = 0; i < NUM_EXAMPLES; i++) {
lv_obj_t* btn = lv_list_add_button(list, LV_SYMBOL_PLAY, examples[i].name);
lv_obj_add_event_cb(btn, onExampleSelected, LV_EVENT_CLICKED, (void*)(intptr_t)i);
}
char scriptDir[256];
if (!getScriptDir(scriptDir, sizeof(scriptDir))) {
lv_list_add_text(list, "No storage available for .bf files");
return;
}
DIR* dir = opendir(scriptDir);
if (!dir) {
char hint[300];
snprintf(hint, sizeof(hint), "Put .bf files in %s", scriptDir);
lv_list_add_text(list, hint);
return;
}
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
const char* name = entry->d_name;
size_t len = strlen(name);
if (len < 3) continue;
const char* ext3 = &name[len - 3];
const char* ext2 = &name[len - 2];
if (ciStrcmp(ext3, ".bf") != 0 && ciStrcmp(ext2, ".b") != 0) {
continue;
}
size_t pathLen = strlen(scriptDir) + 1 + len + 1;
char* path = (char*)malloc(pathLen);
if (!path) break;
snprintf(path, pathLen, "%s/%s", scriptDir, name);
char** tmp = (char**)realloc(scriptPaths, sizeof(char*) * (scriptCount + 1));
if (!tmp) { free(path); break; }
scriptPaths = tmp;
scriptPaths[scriptCount] = path;
lv_obj_t* btn = lv_list_add_button(list, LV_SYMBOL_FILE, name);
lv_obj_add_event_cb(btn, onFileSelected, LV_EVENT_CLICKED, path);
scriptCount++;
}
closedir(dir);
if (scriptCount == 0) {
lv_list_add_text(list, "No custom scripts found on storage");
}
}
/* ── Lifecycle ────────────────────────────────────────────────────── */
void Brainfuck::onShow(AppHandle app, lv_obj_t* parent) {
g_instance = this;
s_appHandle = app;
state = BfState::Examples;
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
clrBtn = tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onClearClicked, nullptr);
lv_obj_add_flag(clrBtn, LV_OBJ_FLAG_HIDDEN);
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onExamplesClicked, nullptr);
lv_obj_t* cont = lv_obj_create(parent);
lv_obj_set_width(cont, LV_PCT(100));
lv_obj_set_flex_grow(cont, 1);
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(cont, 4, 0);
lv_obj_set_style_pad_gap(cont, 4, 0);
lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_border_width(cont, 0, 0);
outputTa = lv_textarea_create(cont);
lv_textarea_set_text(outputTa, "");
lv_textarea_set_cursor_click_pos(outputTa, false);
lv_obj_add_state(outputTa, LV_STATE_DISABLED);
lv_obj_set_width(outputTa, LV_PCT(100));
lv_obj_set_flex_grow(outputTa, 1);
lv_obj_set_style_text_font(outputTa, lv_font_get_default(), 0);
lv_obj_add_flag(outputTa, LV_OBJ_FLAG_HIDDEN);
inputRow = lv_obj_create(cont);
lv_obj_set_size(inputRow, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(inputRow, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(inputRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(inputRow, 0, 0);
lv_obj_set_style_pad_gap(inputRow, 4, 0);
lv_obj_remove_flag(inputRow, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_border_width(inputRow, 0, 0);
lv_obj_add_flag(inputRow, LV_OBJ_FLAG_HIDDEN);
inputTa = lv_textarea_create(inputRow);
lv_textarea_set_placeholder_text(inputTa, "++++++[>++++++++++<-]>+++++.");
lv_textarea_set_one_line(inputTa, false);
lv_obj_set_flex_grow(inputTa, 1);
lv_obj_set_height(inputTa, 50);
lv_obj_set_style_text_font(inputTa, lv_font_get_default(), 0);
lv_obj_add_event_cb(inputTa, onInputReady, LV_EVENT_READY, nullptr);
lv_obj_t* runBtn = lv_button_create(inputRow);
lv_obj_t* runLbl = lv_label_create(runBtn);
lv_label_set_text(runLbl, LV_SYMBOL_PLAY);
lv_obj_add_event_cb(runBtn, onRunClicked, LV_EVENT_CLICKED, nullptr);
examplesList = lv_list_create(cont);
lv_obj_set_width(examplesList, LV_PCT(100));
lv_obj_set_flex_grow(examplesList, 1);
buildScriptList(examplesList);
}
void Brainfuck::onHide(AppHandle app) {
freeScriptPaths();
outputTa = nullptr;
inputTa = nullptr;
inputRow = nullptr;
examplesList = nullptr;
clrBtn = nullptr;
g_instance = nullptr;
s_appHandle = nullptr;
}