New Apps - Lot 1 (#27)

* New Apps - Lot 1

Trying to mix it up a little with some fun, useful and just weird.
Adds more new apps...
Brainfuck
Breakout
Magic 8 Ball
Todo List

Also re-organised the app name part of main.yml

* Fix build?

* fixes

* and some more

* Update Brainfuck.cpp

* i heard you like fixes with your fixes

* no hard coded paths
This commit is contained in:
Shadowtrance
2026-02-20 04:46:54 +10:00
committed by GitHub
parent 1443a29258
commit a75279c96c
29 changed files with 3215 additions and 1 deletions
+16
View File
@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.20)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
if (DEFINED ENV{TACTILITY_SDK_PATH})
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
else()
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
message(WARNING "TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
endif()
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
project(Brainfuck)
tactility_project(Brainfuck)
+46
View File
@@ -0,0 +1,46 @@
# Brainfuck
A Brainfuck esoteric programming language interpreter for Tactility.
## Overview
Run Brainfuck programs on your device. Includes built-in examples and support for loading custom scripts. Features cycle limit protection to prevent infinite loops from locking up the device.
## Features
- Full Brainfuck VM with 4096-byte tape
- Cycle limit protection (2,000,000 cycles max)
- Built-in examples (Hello World, Fibonacci, Alphabet, Beer song)
- Multi-line code editor
- Execution statistics (cycle count display)
- Load custom scripts from user data directory
## Controls
- **Run Button**: Execute Brainfuck code
- **Enter Key**: Run code from input area
- **Toolbar Buttons**:
- Trash icon: Clear output and input
- List icon: Toggle between examples/scripts list and editor
## Custom Scripts
Place `.bf` or `.b` files (up to 32 KB each) in the app's user data directory to load them from the app. The directory path is shown in the script list when no scripts are found.
## Brainfuck Reference
| Command | Description |
|---------|-------------|
| `>` | Move pointer right |
| `<` | Move pointer left |
| `+` | Increment byte at pointer |
| `-` | Decrement byte at pointer |
| `.` | Output byte as ASCII |
| `,` | Input byte (not supported) |
| `[` | Jump forward past `]` if byte is zero |
| `]` | Jump back to `[` if byte is nonzero |
## Requirements
- Tactility
- Touchscreen or keyboard
+11
View File
@@ -0,0 +1,11 @@
file(GLOB_RECURSE SOURCE_FILES
Source/*.c*
)
idf_component_register(
SRCS ${SOURCE_FILES}
# Library headers must be included directly,
# because all regular dependencies get stripped by elf_loader's cmake script
INCLUDE_DIRS ../../../Libraries/TactilityCpp/Include
REQUIRES TactilitySDK
)
+461
View File
@@ -0,0 +1,461 @@
#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;
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;
}
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include <tt_app.h>
#include <lvgl.h>
#include <TactilityCpp/App.h>
constexpr int TAPE_SIZE = 4096;
constexpr int MAX_OUTPUT = 2048;
constexpr int MAX_CYCLES = 2000000;
struct BfVM {
uint8_t tape[TAPE_SIZE];
int ptr;
int pc;
int cycles;
char output[MAX_OUTPUT];
int outLen;
bool error;
char errorMsg[64];
};
enum class BfState {
Main,
Examples,
};
class Brainfuck final : public App {
private:
// UI pointers (nulled in onHide)
lv_obj_t* outputTa = nullptr;
lv_obj_t* inputTa = nullptr;
lv_obj_t* inputRow = nullptr;
lv_obj_t* examplesList = nullptr;
lv_obj_t* clrBtn = nullptr;
BfState state = BfState::Examples;
BfVM vm = {};
// Helper methods
void bfInit();
void bfRun(const char* code);
void runCode(const char* code);
void buildScriptList(lv_obj_t* list);
// View management
void showMainView();
void showExamplesView();
// Static callbacks
static void onRunClicked(lv_event_t* e);
static void onClearClicked(lv_event_t* e);
static void onExamplesClicked(lv_event_t* e);
static void onExampleSelected(lv_event_t* e);
static void onFileSelected(lv_event_t* e);
static void onInputReady(lv_event_t* e);
public:
void onShow(AppHandle context, lv_obj_t* parent) override;
void onHide(AppHandle context) override;
};
+11
View File
@@ -0,0 +1,11 @@
#include "Brainfuck.h"
#include <TactilityCpp/App.h>
extern "C" {
int main(int argc, char* argv[]) {
registerApp<Brainfuck>();
return 0;
}
}
+10
View File
@@ -0,0 +1,10 @@
[manifest]
version=0.1
[target]
sdk=0.7.0-dev
platforms=esp32,esp32s3,esp32c6,esp32p4
[app]
id=one.tactility.brainfuck
versionName=0.1.0
versionCode=1
name=Brainfuck esoteric language interpreter