Просмотр исходного кода

Add brainfuck from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: brainfuck
git-subtree-mainline: 35e5bf79f7ca8dc2273fa62540fd45dda059c600
git-subtree-split: 5d84a593e275c7bafd21abf10594c03e3195692e
Willy-JL 2 лет назад
Родитель
Сommit
aa3f15e638

+ 6 - 0
brainfuck/.gitignore

@@ -0,0 +1,6 @@
+
+README.md
+.vscode/c_cpp_properties.json
+.vscode/launch.json
+README.md
+README.md

+ 1 - 0
brainfuck/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev apps_source_code/brainfuck

+ 18 - 0
brainfuck/application.fam

@@ -0,0 +1,18 @@
+App(
+    appid="brainfuck",
+    name="Brainfuck",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="brainfuck_app",
+    requires=[
+        "storage",
+        "gui",
+    ],
+    stack_size=8 * 1024,
+    fap_icon="bfico.png",
+    fap_category="Tools",
+    fap_icon_assets="icons",
+    fap_author="@nymda",
+    fap_weburl="https://github.com/nymda/FlipperZeroBrainfuck",
+    fap_version="1.1",
+    fap_description="Brainfuck language interpreter",
+)

BIN
brainfuck/bfico.png


+ 148 - 0
brainfuck/brainfuck.c

@@ -0,0 +1,148 @@
+#include "brainfuck_i.h"
+
+/*
+    Due to the lack of documentation on the flipper i copied the picopass app,
+    ripped its insides out and used its hollow corpse to build this app inside of.
+
+    i dont know how this stuff works and after 6 hours of trying to learn it, i dont care
+*/
+
+bool brainfuck_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    BFApp* brainfuck = context;
+    return scene_manager_handle_custom_event(brainfuck->scene_manager, event);
+}
+
+bool brainfuck_back_event_callback(void* context) {
+    furi_assert(context);
+    BFApp* brainfuck = context;
+    return scene_manager_handle_back_event(brainfuck->scene_manager);
+}
+
+BFApp* brainfuck_alloc() {
+    BFApp* brainfuck = malloc(sizeof(BFApp));
+
+    brainfuck->dataSize = 0;
+    brainfuck->view_dispatcher = view_dispatcher_alloc();
+    brainfuck->scene_manager = scene_manager_alloc(&brainfuck_scene_handlers, brainfuck);
+    view_dispatcher_enable_queue(brainfuck->view_dispatcher);
+    view_dispatcher_set_event_callback_context(brainfuck->view_dispatcher, brainfuck);
+    view_dispatcher_set_custom_event_callback(
+        brainfuck->view_dispatcher, brainfuck_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        brainfuck->view_dispatcher, brainfuck_back_event_callback);
+
+    // Open GUI record
+    brainfuck->gui = furi_record_open(RECORD_GUI);
+    view_dispatcher_attach_to_gui(
+        brainfuck->view_dispatcher, brainfuck->gui, ViewDispatcherTypeFullscreen);
+
+    // Open Notification record
+    brainfuck->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    // Submenu
+    brainfuck->submenu = submenu_alloc();
+    view_dispatcher_add_view(
+        brainfuck->view_dispatcher, brainfuckViewMenu, submenu_get_view(brainfuck->submenu));
+
+    // Popup
+    brainfuck->popup = popup_alloc();
+    view_dispatcher_add_view(
+        brainfuck->view_dispatcher, brainfuckViewPopup, popup_get_view(brainfuck->popup));
+
+    // Text Input
+    brainfuck->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        brainfuck->view_dispatcher,
+        brainfuckViewTextInput,
+        text_input_get_view(brainfuck->text_input));
+
+    // Textbox
+    brainfuck->text_box = text_box_alloc();
+    view_dispatcher_add_view(
+        brainfuck->view_dispatcher, brainfuckViewTextBox, text_box_get_view(brainfuck->text_box));
+    brainfuck->text_box_store = furi_string_alloc();
+
+    // Dev environment
+    brainfuck->BF_dev_env = bf_dev_env_alloc(brainfuck);
+    view_dispatcher_add_view(
+        brainfuck->view_dispatcher, brainfuckViewDev, bf_dev_env_get_view(brainfuck->BF_dev_env));
+
+    // File path
+    brainfuck->BF_file_path = furi_string_alloc();
+
+    return brainfuck;
+}
+
+void brainfuck_free(BFApp* brainfuck) {
+    furi_assert(brainfuck);
+
+    // Submenu
+    view_dispatcher_remove_view(brainfuck->view_dispatcher, brainfuckViewMenu);
+    submenu_free(brainfuck->submenu);
+
+    // Popup
+    view_dispatcher_remove_view(brainfuck->view_dispatcher, brainfuckViewPopup);
+    popup_free(brainfuck->popup);
+
+    // TextInput
+    view_dispatcher_remove_view(brainfuck->view_dispatcher, brainfuckViewTextInput);
+    text_input_free(brainfuck->text_input);
+
+    // TextBox
+    view_dispatcher_remove_view(brainfuck->view_dispatcher, brainfuckViewTextBox);
+    text_box_free(brainfuck->text_box);
+    furi_string_free(brainfuck->text_box_store);
+
+    //dev env
+    view_dispatcher_remove_view(brainfuck->view_dispatcher, brainfuckViewDev);
+    bf_dev_env_free(brainfuck->BF_dev_env);
+
+    // View Dispatcher
+    view_dispatcher_free(brainfuck->view_dispatcher);
+
+    // Scene Manager
+    scene_manager_free(brainfuck->scene_manager);
+
+    // GUI
+    furi_record_close(RECORD_GUI);
+    brainfuck->gui = NULL;
+
+    // Notifications
+    furi_record_close(RECORD_NOTIFICATION);
+    brainfuck->notifications = NULL;
+
+    free(brainfuck);
+}
+
+void brainfuck_show_loading_popup(void* context, bool show) {
+    BFApp* brainfuck = context;
+
+    if(show) {
+        // Raise timer priority so that animations can play
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+        view_dispatcher_switch_to_view(brainfuck->view_dispatcher, brainfuckViewLoading);
+    } else {
+        // Restore default timer priority
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
+    }
+}
+
+int32_t brainfuck_app(void* p) {
+    UNUSED(p);
+    BFApp* brainfuck = brainfuck_alloc();
+    if(!brainfuck) {
+        return 0;
+    }
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    storage_simply_mkdir(storage, "/ext/brainfuck");
+
+    scene_manager_next_scene(brainfuck->scene_manager, brainfuckSceneStart);
+
+    view_dispatcher_run(brainfuck->view_dispatcher);
+
+    brainfuck_free(brainfuck);
+
+    return 0;
+}

+ 3 - 0
brainfuck/brainfuck.h

@@ -0,0 +1,3 @@
+#pragma once
+
+typedef struct BFApp BFApp;

+ 88 - 0
brainfuck/brainfuck_i.h

@@ -0,0 +1,88 @@
+#pragma once
+
+typedef struct BFDevEnv BFDevEnv;
+typedef struct BFExecEnv BFExecEnv;
+typedef unsigned char byte;
+
+#include "brainfuck.h"
+#include "worker.h"
+
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <notification/notification_messages.h>
+
+#include <gui/modules/submenu.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/loading.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/text_box.h>
+
+#include <dialogs/dialogs.h>
+#include <input/input.h>
+
+#include "scenes/brainfuck_scene.h"
+
+#include "views/bf_dev_env.h"
+
+#include <storage/storage.h>
+#include <lib/toolbox/path.h>
+#include <brainfuck_icons.h>
+
+#include <storage/storage.h>
+#include <stream/stream.h>
+#include <stream/buffered_file_stream.h>
+#include <toolbox/stream/file_stream.h>
+
+#include <notification/notification_messages.h>
+
+#define BF_INST_BUFFER_SIZE 2048
+#define BF_OUTPUT_SIZE 512
+#define BF_STACK_INITIAL_SIZE 128
+#define BF_INPUT_BUFFER_SIZE 64
+#define BF_STACK_STEP_SIZE 32
+
+enum brainfuckCustomEvent {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    brainfuckCustomEventReserved = 100,
+
+    brainfuckCustomEventViewExit,
+    brainfuckCustomEventWorkerExit,
+    brainfuckCustomEventByteInputDone,
+    brainfuckCustomEventTextInputDone,
+};
+
+typedef enum {
+    EventTypeTick,
+    EventTypeKey,
+} EventType;
+
+struct BFApp {
+    ViewDispatcher* view_dispatcher;
+    Gui* gui;
+    NotificationApp* notifications;
+    SceneManager* scene_manager;
+    Submenu* submenu;
+    Popup* popup;
+    TextInput* text_input;
+    TextBox* text_box;
+    FuriString* text_box_store;
+    FuriString* BF_file_path;
+    BFDevEnv* BF_dev_env;
+    int dataSize;
+    char dataBuffer[BF_INST_BUFFER_SIZE];
+    char inputBuffer[BF_INPUT_BUFFER_SIZE];
+};
+
+typedef enum {
+    brainfuckViewMenu,
+    brainfuckViewPopup,
+    brainfuckViewLoading,
+    brainfuckViewTextInput,
+    brainfuckViewTextBox,
+    brainfuckViewWidget,
+    brainfuckViewDev,
+    brainfuckViewExec,
+} brainfuckView;

BIN
brainfuck/icons/ButtonRightSmall_3x5.png


BIN
brainfuck/icons/KeyBackspaceSelected_24x11.png


BIN
brainfuck/icons/KeyBackspace_24x11.png


BIN
brainfuck/icons/KeyInputSelected_30x11.png


BIN
brainfuck/icons/KeyInput_30x11.png


BIN
brainfuck/icons/KeyRunSelected_24x11.png


BIN
brainfuck/icons/KeyRun_24x11.png


BIN
brainfuck/icons/KeySaveSelected_24x11.png


BIN
brainfuck/icons/KeySave_24x11.png


BIN
brainfuck/icons/bfico.png


BIN
brainfuck/img/1.png


BIN
brainfuck/img/2.png


BIN
brainfuck/img/3.png


+ 30 - 0
brainfuck/scenes/brainfuck_scene.c

@@ -0,0 +1,30 @@
+#include "brainfuck_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const brainfuck_on_enter_handlers[])(void*) = {
+#include "brainfuck_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const brainfuck_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "brainfuck_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const brainfuck_on_exit_handlers[])(void* context) = {
+#include "brainfuck_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers brainfuck_scene_handlers = {
+    .on_enter_handlers = brainfuck_on_enter_handlers,
+    .on_event_handlers = brainfuck_on_event_handlers,
+    .on_exit_handlers = brainfuck_on_exit_handlers,
+    .scene_num = brainfuckSceneNum,
+};

+ 29 - 0
brainfuck/scenes/brainfuck_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) brainfuckScene##id,
+typedef enum {
+#include "brainfuck_scene_config.h"
+    brainfuckSceneNum,
+} brainfuckScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers brainfuck_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "brainfuck_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "brainfuck_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "brainfuck_scene_config.h"
+#undef ADD_SCENE

+ 6 - 0
brainfuck/scenes/brainfuck_scene_config.h

@@ -0,0 +1,6 @@
+ADD_SCENE(brainfuck, start, Start)
+ADD_SCENE(brainfuck, file_select, FileSelect)
+ADD_SCENE(brainfuck, file_create, FileCreate)
+ADD_SCENE(brainfuck, dev_env, DevEnv)
+ADD_SCENE(brainfuck, exec_env, ExecEnv)
+ADD_SCENE(brainfuck, set_input, SetInput)

+ 16 - 0
brainfuck/scenes/brainfuck_scene_dev.c

@@ -0,0 +1,16 @@
+#include "../brainfuck_i.h"
+
+void brainfuck_scene_dev_env_on_enter(void* context) {
+    BFApp* app = context;
+    view_dispatcher_switch_to_view(app->view_dispatcher, brainfuckViewDev);
+}
+
+bool brainfuck_scene_dev_env_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void brainfuck_scene_dev_env_on_exit(void* context) {
+    UNUSED(context);
+}

+ 16 - 0
brainfuck/scenes/brainfuck_scene_exec.c

@@ -0,0 +1,16 @@
+#include "../brainfuck_i.h"
+
+void brainfuck_scene_exec_env_on_enter(void* context) {
+    BFApp* app = context;
+    view_dispatcher_switch_to_view(app->view_dispatcher, brainfuckViewTextBox);
+}
+
+bool brainfuck_scene_exec_env_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void brainfuck_scene_exec_env_on_exit(void* context) {
+    UNUSED(context);
+}

+ 50 - 0
brainfuck/scenes/brainfuck_scene_file_create.c

@@ -0,0 +1,50 @@
+#include "../brainfuck_i.h"
+
+void file_name_text_input_callback(void* context) {
+    BFApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, brainfuckCustomEventTextInputDone);
+}
+
+char tmpName[64] = {};
+byte empty[1] = {0x00};
+void brainfuck_scene_file_create_on_enter(void* context) {
+    BFApp* app = context;
+    TextInput* text_input = app->text_input;
+
+    text_input_set_header_text(text_input, "New script name");
+    text_input_set_result_callback(
+        text_input, file_name_text_input_callback, app, tmpName, 64, true);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, brainfuckViewTextInput);
+}
+
+bool brainfuck_scene_file_create_on_event(void* context, SceneManagerEvent event) {
+    BFApp* app = context;
+    UNUSED(app);
+
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == brainfuckCustomEventTextInputDone) {
+            furi_string_cat_printf(app->BF_file_path, "/ext/brainfuck/%s.b", tmpName);
+
+            //remove old file
+            Storage* storage = furi_record_open(RECORD_STORAGE);
+            storage_simply_remove(storage, furi_string_get_cstr(app->BF_file_path));
+
+            //save new file
+            Stream* stream = buffered_file_stream_alloc(storage);
+            buffered_file_stream_open(
+                stream, furi_string_get_cstr(app->BF_file_path), FSAM_WRITE, FSOM_CREATE_ALWAYS);
+            stream_write(stream, (const uint8_t*)empty, 1);
+            buffered_file_stream_close(stream);
+
+            //scene_manager_next_scene(app->scene_manager, brainfuckSceneFileSelect);
+            scene_manager_next_scene(app->scene_manager, brainfuckSceneDevEnv);
+        }
+    }
+    return consumed;
+}
+
+void brainfuck_scene_file_create_on_exit(void* context) {
+    UNUSED(context);
+}

+ 34 - 0
brainfuck/scenes/brainfuck_scene_file_select.c

@@ -0,0 +1,34 @@
+#include "../brainfuck_i.h"
+
+void brainfuck_scene_file_select_on_enter(void* context) {
+    BFApp* app = context;
+
+    DialogsApp* dialogs = furi_record_open("dialogs");
+    FuriString* path;
+    path = furi_string_alloc();
+    furi_string_set(path, "/ext/brainfuck");
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, ".b", &I_bfico);
+    browser_options.base_path = "/ext/brainfuck";
+    browser_options.hide_ext = false;
+
+    bool selected = dialog_file_browser_show(dialogs, path, path, &browser_options);
+
+    if(selected) {
+        furi_string_set(app->BF_file_path, path);
+        scene_manager_next_scene(app->scene_manager, brainfuckSceneDevEnv);
+    } else {
+        scene_manager_search_and_switch_to_previous_scene(app->scene_manager, brainfuckSceneStart);
+    }
+}
+
+bool brainfuck_scene_file_select_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void brainfuck_scene_file_select_on_exit(void* context) {
+    UNUSED(context);
+}

+ 35 - 0
brainfuck/scenes/brainfuck_scene_set_input.c

@@ -0,0 +1,35 @@
+#include "../brainfuck_i.h"
+
+void set_input_text_input_callback(void* context) {
+    BFApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, brainfuckCustomEventTextInputDone);
+}
+
+void brainfuck_scene_set_input_on_enter(void* context) {
+    BFApp* app = context;
+    TextInput* text_input = app->text_input;
+
+    text_input_set_header_text(text_input, "Edit input buffer");
+    text_input_set_result_callback(
+        text_input, set_input_text_input_callback, app, app->inputBuffer, 64, true);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, brainfuckViewTextInput);
+}
+
+bool brainfuck_scene_set_input_on_event(void* context, SceneManagerEvent event) {
+    BFApp* app = context;
+
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == brainfuckCustomEventTextInputDone) {
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, brainfuckSceneDevEnv);
+        }
+    }
+    return consumed;
+}
+
+void brainfuck_scene_set_input_on_exit(void* context) {
+    BFApp* app = context;
+    scene_manager_search_and_switch_to_previous_scene(app->scene_manager, brainfuckSceneDevEnv);
+}

+ 55 - 0
brainfuck/scenes/brainfuck_scene_start.c

@@ -0,0 +1,55 @@
+#include "../brainfuck_i.h"
+enum SubmenuIndex {
+    SubmenuIndexNew,
+    SubmenuIndexOpen,
+    SubmenuIndexAbout,
+};
+
+void brainfuck_scene_start_submenu_callback(void* context, uint32_t index) {
+    BFApp* brainfuck = context;
+    view_dispatcher_send_custom_event(brainfuck->view_dispatcher, index);
+}
+void brainfuck_scene_start_on_enter(void* context) {
+    BFApp* brainfuck = context;
+
+    Submenu* submenu = brainfuck->submenu;
+    submenu_add_item(
+        submenu, "New", SubmenuIndexNew, brainfuck_scene_start_submenu_callback, brainfuck);
+    submenu_add_item(
+        submenu, "Open", SubmenuIndexOpen, brainfuck_scene_start_submenu_callback, brainfuck);
+    submenu_add_item(
+        submenu, "About", SubmenuIndexAbout, brainfuck_scene_start_submenu_callback, brainfuck);
+
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(brainfuck->scene_manager, brainfuckSceneStart));
+    view_dispatcher_switch_to_view(brainfuck->view_dispatcher, brainfuckViewMenu);
+}
+
+bool brainfuck_scene_start_on_event(void* context, SceneManagerEvent event) {
+    BFApp* brainfuck = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexNew) {
+            scene_manager_next_scene(brainfuck->scene_manager, brainfuckSceneFileCreate);
+            consumed = true;
+        } else if(event.event == SubmenuIndexOpen) {
+            scene_manager_next_scene(brainfuck->scene_manager, brainfuckSceneFileSelect);
+            consumed = true;
+        } else if(event.event == SubmenuIndexAbout) {
+            text_box_set_text(
+                brainfuck->text_box,
+                "FlipperBrainfuck\n\nAn F0 brainfuck intepretor\nBy github.com/Nymda");
+            scene_manager_next_scene(brainfuck->scene_manager, brainfuckSceneExecEnv);
+            consumed = true;
+        }
+        scene_manager_set_scene_state(brainfuck->scene_manager, brainfuckSceneStart, event.event);
+    }
+
+    return consumed;
+}
+
+void brainfuck_scene_start_on_exit(void* context) {
+    BFApp* brainfuck = context;
+    submenu_reset(brainfuck->submenu);
+}

+ 429 - 0
brainfuck/views/bf_dev_env.c

@@ -0,0 +1,429 @@
+#include "bf_dev_env.h"
+#include <gui/elements.h>
+
+typedef struct BFDevEnv {
+    View* view;
+    DevEnvOkCallback callback;
+    void* context;
+    BFApp* appDev;
+} BFDevEnv;
+
+typedef struct {
+    uint32_t row;
+    uint32_t col;
+} BFDevEnvModel;
+
+typedef struct {
+    int up;
+    int down;
+    int left;
+    int right;
+} bMapping;
+
+#ifdef FW_ORIGIN_Official
+#define FONT_NAME FontSecondary
+#else
+#define FONT_NAME FontBatteryPercent
+#endif
+
+static bool bf_dev_process_up(BFDevEnv* devEnv);
+static bool bf_dev_process_down(BFDevEnv* devEnv);
+static bool bf_dev_process_left(BFDevEnv* devEnv);
+static bool bf_dev_process_right(BFDevEnv* devEnv);
+static bool bf_dev_process_ok(BFDevEnv* devEnv, InputEvent* event);
+
+BFApp* appDev;
+FuriThread* workerThread;
+
+char bfChars[9] = {'<', '>', '[', ']', '+', '-', '.', ',', 0x00};
+
+int selectedButton = 0;
+int saveNotifyCountdown = 0;
+int execCountdown = 0;
+
+char dspLine0[25] = {0x00};
+char dspLine1[25] = {0x00};
+char dspLine2[25] = {0x00};
+
+static bMapping buttonMappings[12] = {
+    {8, 8, 7, 1}, //0
+    {8, 8, 0, 2}, //1
+    {9, 9, 1, 3}, //2
+    {9, 9, 2, 4}, //3
+    {10, 10, 3, 5}, //4
+    {10, 10, 4, 6}, //5
+    {11, 11, 5, 7}, //6
+    {11, 11, 6, 0}, //7
+
+    {0, 0, 11, 9}, //8
+    {3, 3, 8, 10}, //9
+    {5, 5, 9, 11}, //10
+    {6, 6, 10, 8} //11
+};
+
+#define BT_X 14
+#define BT_Y 14
+static void bf_dev_draw_button(Canvas* canvas, int x, int y, bool selected, const char* lbl) {
+    UNUSED(lbl);
+
+    if(selected) {
+        canvas_draw_rbox(canvas, x, y, BT_X, BT_Y, 3);
+        canvas_invert_color(canvas);
+        canvas_set_font(canvas, FONT_NAME);
+        canvas_draw_str_aligned(
+            canvas, x + (BT_X / 2), y + (BT_Y / 2) - 1, AlignCenter, AlignCenter, lbl);
+        canvas_invert_color(canvas);
+    } else {
+        canvas_draw_rbox(canvas, x, y, BT_X, BT_Y, 3);
+        canvas_invert_color(canvas);
+        canvas_draw_rbox(canvas, x + 2, y - 1, BT_X - 2, BT_Y - 1, 3);
+        canvas_invert_color(canvas);
+        canvas_draw_rframe(canvas, x, y, BT_X, BT_Y, 3);
+        canvas_set_font(canvas, FONT_NAME);
+        canvas_draw_str_aligned(
+            canvas, x + (BT_X / 2), y + (BT_Y / 2) - 1, AlignCenter, AlignCenter, lbl);
+    }
+}
+
+void bf_save_changes() {
+    //remove old file
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    storage_simply_remove(storage, furi_string_get_cstr(appDev->BF_file_path));
+
+    //save new file
+    Stream* stream = buffered_file_stream_alloc(storage);
+    buffered_file_stream_open(
+        stream, furi_string_get_cstr(appDev->BF_file_path), FSAM_WRITE, FSOM_CREATE_ALWAYS);
+    stream_write(stream, (const uint8_t*)appDev->dataBuffer, appDev->dataSize);
+    buffered_file_stream_close(stream);
+}
+
+static void bf_dev_draw_callback(Canvas* canvas, void* _model) {
+    UNUSED(_model);
+
+    if(saveNotifyCountdown > 0) {
+        canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignCenter, "SAVED");
+        saveNotifyCountdown--;
+        return;
+    }
+
+    bf_dev_draw_button(canvas, 1, 36, (selectedButton == 0), "+"); //T 0
+    bf_dev_draw_button(canvas, 17, 36, (selectedButton == 1), "-"); //T 1
+    bf_dev_draw_button(canvas, 33, 36, (selectedButton == 2), "<"); //T 2
+    bf_dev_draw_button(canvas, 49, 36, (selectedButton == 3), ">"); //T 3
+    bf_dev_draw_button(canvas, 65, 36, (selectedButton == 4), "["); //B 0
+    bf_dev_draw_button(canvas, 81, 36, (selectedButton == 5), "]"); //B 1
+    bf_dev_draw_button(canvas, 97, 36, (selectedButton == 6), "."); //B 2
+    bf_dev_draw_button(canvas, 113, 36, (selectedButton == 7), ","); //B 3
+
+    //backspace, input, run, save
+    canvas_draw_icon(
+        canvas,
+        1,
+        52,
+        (selectedButton == 8) ? &I_KeyBackspaceSelected_24x11 : &I_KeyBackspace_24x11);
+    canvas_draw_icon(
+        canvas, 45, 52, (selectedButton == 9) ? &I_KeyInputSelected_30x11 : &I_KeyInput_30x11);
+    canvas_draw_icon(
+        canvas, 77, 52, (selectedButton == 10) ? &I_KeyRunSelected_24x11 : &I_KeyRun_24x11);
+    canvas_draw_icon(
+        canvas, 103, 52, (selectedButton == 11) ? &I_KeySaveSelected_24x11 : &I_KeySave_24x11);
+
+    if(saveNotifyCountdown > 0) {
+        canvas_draw_icon(canvas, 98, 54, &I_ButtonRightSmall_3x5);
+        saveNotifyCountdown--;
+    }
+
+    //textbox
+    //grossly overcomplicated. not fixing it.
+    canvas_draw_rframe(canvas, 1, 1, 126, 33, 2);
+    canvas_set_font(canvas, FONT_NAME);
+
+    int dbOffset = 0;
+    if(appDev->dataSize > 72) {
+        dbOffset = (appDev->dataSize - 72);
+    }
+
+    memset(dspLine0, 0x00, 25);
+    memset(dspLine1, 0x00, 25);
+    memset(dspLine2, 0x00, 25);
+
+    int tpM = 0;
+    int tp0 = 0;
+    int tp1 = 0;
+    int tp2 = 0;
+
+    for(int p = dbOffset; p < appDev->dataSize; p++) {
+        if(tpM < 24 * 1) {
+            dspLine0[tp0] = appDev->dataBuffer[p];
+            tp0++;
+        } else if(tpM < 24 * 2) {
+            dspLine1[tp1] = appDev->dataBuffer[p];
+            tp1++;
+        } else if(tpM < 24 * 3) {
+            dspLine2[tp2] = appDev->dataBuffer[p];
+            tp2++;
+        }
+        tpM++;
+    }
+
+    canvas_draw_str_aligned(canvas, 3, 8, AlignLeft, AlignCenter, dspLine0);
+    canvas_draw_str_aligned(canvas, 3, 17, AlignLeft, AlignCenter, dspLine1);
+    canvas_draw_str_aligned(canvas, 3, 26, AlignLeft, AlignCenter, dspLine2);
+}
+
+static bool bf_dev_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    BFDevEnv* devEnv = context;
+    bool consumed = false;
+
+    if(event->type == InputTypeShort) {
+        if(event->key == InputKeyRight) {
+            consumed = bf_dev_process_right(devEnv);
+        } else if(event->key == InputKeyLeft) {
+            consumed = bf_dev_process_left(devEnv);
+        } else if(event->key == InputKeyUp) {
+            consumed = bf_dev_process_up(devEnv);
+        } else if(event->key == InputKeyDown) {
+            consumed = bf_dev_process_down(devEnv);
+        }
+    } else if(event->key == InputKeyOk) {
+        consumed = bf_dev_process_ok(devEnv, event);
+    }
+
+    return consumed;
+}
+
+static bool bf_dev_process_up(BFDevEnv* devEnv) {
+    UNUSED(devEnv);
+    selectedButton = buttonMappings[selectedButton].up;
+    return true;
+}
+
+static bool bf_dev_process_down(BFDevEnv* devEnv) {
+    UNUSED(devEnv);
+    selectedButton = buttonMappings[selectedButton].down;
+    return true;
+}
+
+static bool bf_dev_process_left(BFDevEnv* devEnv) {
+    UNUSED(devEnv);
+    selectedButton = buttonMappings[selectedButton].left;
+    return true;
+}
+
+static bool bf_dev_process_right(BFDevEnv* devEnv) {
+    UNUSED(devEnv);
+    selectedButton = buttonMappings[selectedButton].right;
+    return true;
+}
+
+static bool bf_dev_process_ok(BFDevEnv* devEnv, InputEvent* event) {
+    UNUSED(devEnv);
+    UNUSED(event);
+
+    if(event->type != InputTypePress) {
+        return false;
+    }
+
+    switch(selectedButton) {
+    case 0: {
+        if(appDev->dataSize < BF_INST_BUFFER_SIZE) {
+            appDev->dataBuffer[appDev->dataSize] = '+';
+            appDev->dataSize++;
+        }
+        break;
+    }
+
+    case 1: {
+        if(appDev->dataSize < BF_INST_BUFFER_SIZE) {
+            appDev->dataBuffer[appDev->dataSize] = '-';
+            appDev->dataSize++;
+        }
+        break;
+    }
+
+    case 2: {
+        if(appDev->dataSize < BF_INST_BUFFER_SIZE) {
+            appDev->dataBuffer[appDev->dataSize] = '<';
+            appDev->dataSize++;
+        }
+        break;
+    }
+
+    case 3: {
+        if(appDev->dataSize < BF_INST_BUFFER_SIZE) {
+            appDev->dataBuffer[appDev->dataSize] = '>';
+            appDev->dataSize++;
+        }
+        break;
+    }
+
+    case 4: {
+        if(appDev->dataSize < BF_INST_BUFFER_SIZE) {
+            appDev->dataBuffer[appDev->dataSize] = '[';
+            appDev->dataSize++;
+        }
+        break;
+    }
+
+    case 5: {
+        if(appDev->dataSize < BF_INST_BUFFER_SIZE) {
+            appDev->dataBuffer[appDev->dataSize] = ']';
+            appDev->dataSize++;
+        }
+        break;
+    }
+
+    case 6: {
+        if(appDev->dataSize < BF_INST_BUFFER_SIZE) {
+            appDev->dataBuffer[appDev->dataSize] = '.';
+            appDev->dataSize++;
+        }
+        break;
+    }
+
+    case 7: {
+        if(appDev->dataSize < BF_INST_BUFFER_SIZE) {
+            appDev->dataBuffer[appDev->dataSize] = ',';
+            appDev->dataSize++;
+        }
+        break;
+    }
+
+    case 8: {
+        if(appDev->dataSize > 0) {
+            appDev->dataSize--;
+            appDev->dataBuffer[appDev->dataSize] = (uint32_t)0x00;
+        }
+        break;
+    }
+
+    case 9: {
+        scene_manager_next_scene(appDev->scene_manager, brainfuckSceneSetInput);
+        break;
+    }
+
+    case 10: {
+        if(getStatus() != 0) {
+            killThread();
+            furi_thread_join(workerThread);
+        }
+
+        bf_save_changes();
+
+        initWorker(appDev);
+        text_box_set_focus(appDev->text_box, TextBoxFocusEnd);
+        text_box_set_text(appDev->text_box, workerGetOutput());
+
+        workerThread = furi_thread_alloc_ex("Worker", 2048, (void*)beginWorker, NULL);
+        furi_thread_start(workerThread);
+
+        scene_manager_next_scene(appDev->scene_manager, brainfuckSceneExecEnv);
+        break;
+    }
+
+    case 11: {
+        bf_save_changes();
+        saveNotifyCountdown = 3;
+        break;
+    }
+    }
+
+    bool consumed = false;
+    return consumed;
+}
+
+static void bf_dev_enter_callback(void* context) {
+    furi_assert(context);
+    BFDevEnv* devEnv = context;
+
+    with_view_model(
+        devEnv->view,
+        BFDevEnvModel * model,
+        {
+            model->col = 0;
+            model->row = 0;
+        },
+        true);
+
+    appDev = devEnv->appDev;
+    selectedButton = 0;
+
+    //exit the running thread if required
+    if(getStatus() != 0) {
+        killThread();
+        furi_thread_join(workerThread);
+    }
+
+    //clear the bf instruction buffer
+    memset(appDev->dataBuffer, 0x00, BF_INST_BUFFER_SIZE * sizeof(char));
+
+    //open the file
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    Stream* stream = buffered_file_stream_alloc(storage);
+    buffered_file_stream_open(
+        stream, furi_string_get_cstr(appDev->BF_file_path), FSAM_READ, FSOM_OPEN_EXISTING);
+
+    //read into the buffer
+    appDev->dataSize = stream_size(stream);
+    if(appDev->dataSize > 2000) {
+        return; //BF file is too large
+    }
+
+    stream_read(stream, (uint8_t*)appDev->dataBuffer, appDev->dataSize);
+    buffered_file_stream_close(stream);
+
+    //replaces any invalid characters with an underscore. strips out newlines, comments, etc
+    for(int i = 0; i < appDev->dataSize; i++) {
+        if(!strchr(bfChars, appDev->dataBuffer[i])) {
+            appDev->dataBuffer[i] = '_';
+        }
+    }
+
+    //find the end of the file to begin editing
+    int tptr = 0;
+    while(appDev->dataBuffer[tptr] != 0x00) {
+        tptr++;
+    }
+    appDev->dataSize = tptr;
+}
+
+BFDevEnv* bf_dev_env_alloc(BFApp* appDev) {
+    BFDevEnv* devEnv = malloc(sizeof(BFDevEnv));
+
+    devEnv->view = view_alloc();
+    devEnv->appDev = appDev;
+    view_allocate_model(devEnv->view, ViewModelTypeLocking, sizeof(BFDevEnvModel));
+
+    with_view_model(
+        devEnv->view,
+        BFDevEnvModel * model,
+        {
+            model->col = 0;
+            model->row = 0;
+        },
+        true);
+
+    view_set_context(devEnv->view, devEnv);
+    view_set_draw_callback(devEnv->view, bf_dev_draw_callback);
+    view_set_input_callback(devEnv->view, bf_dev_input_callback);
+    view_set_enter_callback(devEnv->view, bf_dev_enter_callback);
+    return devEnv;
+}
+
+void bf_dev_env_free(BFDevEnv* devEnv) {
+    if(getStatus() != 0) {
+        killThread();
+        furi_thread_join(workerThread);
+    }
+
+    furi_assert(devEnv);
+    view_free(devEnv->view);
+    free(devEnv);
+}
+
+View* bf_dev_env_get_view(BFDevEnv* devEnv) {
+    furi_assert(devEnv);
+    return devEnv->view;
+}

+ 15 - 0
brainfuck/views/bf_dev_env.h

@@ -0,0 +1,15 @@
+#pragma once
+#include "../brainfuck_i.h"
+#include <gui/view.h>
+
+typedef void (*DevEnvOkCallback)(InputType type, void* context);
+
+BFDevEnv* bf_dev_env_alloc(BFApp* application);
+
+void bf_dev_set_file_path(FuriString* path);
+
+void bf_dev_env_free(BFDevEnv* devEnv);
+
+View* bf_dev_env_get_view(BFDevEnv* devEnv);
+
+void bf_dev_env_set_ok(BFDevEnv* devEnv, DevEnvOkCallback callback, void* context);

+ 291 - 0
brainfuck/worker.c

@@ -0,0 +1,291 @@
+#include "worker.h"
+#include <furi_hal_resources.h>
+#include <furi.h>
+
+bool killswitch = false;
+
+int status = 0; //0: idle, 1: running, 2: failure
+
+char* inst = 0;
+int instCount = 0;
+int instPtr = 0;
+int runOpCount = 0;
+
+char* wOutput = 0;
+int wOutputPtr = 0;
+
+char* wInput = 0;
+int wInputPtr = 0;
+
+uint8_t* bfStack = 0;
+int stackPtr = 0;
+int stackSize = BF_STACK_INITIAL_SIZE;
+int stackSizeReal = 0;
+
+BFApp* wrkrApp = 0;
+
+void killThread() {
+    killswitch = true;
+}
+
+bool validateInstPtr() {
+    if(instPtr > instCount || instPtr < 0) {
+        return false;
+    }
+    return true;
+}
+
+bool validateStackPtr() {
+    if(stackPtr > stackSize || stackPtr < 0) {
+        return false;
+    }
+    return true;
+}
+
+char* workerGetOutput() {
+    return wOutput;
+}
+
+int getStackSize() {
+    return stackSizeReal;
+}
+
+int getOpCount() {
+    return runOpCount;
+}
+
+int getStatus() {
+    return status;
+}
+
+void initWorker(BFApp* app) {
+    wrkrApp = app;
+
+    //rebuild output
+    if(wOutput) {
+        free(wOutput);
+    }
+    wOutput = (char*)malloc(BF_OUTPUT_SIZE);
+    wOutputPtr = 0;
+
+    //rebuild stack
+    if(bfStack) {
+        free(bfStack);
+    }
+    bfStack = (uint8_t*)malloc(BF_STACK_INITIAL_SIZE);
+    memset(bfStack, 0x00, BF_STACK_INITIAL_SIZE);
+    stackSize = BF_STACK_INITIAL_SIZE;
+    stackSizeReal = 0;
+    stackPtr = 0;
+
+    //set instructions
+    inst = wrkrApp->dataBuffer;
+    instCount = wrkrApp->dataSize;
+    instPtr = 0;
+    runOpCount = 0;
+
+    //set input
+    wInput = wrkrApp->inputBuffer;
+    wInputPtr = 0;
+
+    //set status
+    status = 0;
+}
+
+void rShift() {
+    runOpCount++;
+    stackPtr++;
+    if(!validateStackPtr()) {
+        status = 2;
+        return;
+    }
+
+    while(stackPtr > stackSize) {
+        stackSize += BF_STACK_STEP_SIZE;
+        void* tmp = realloc(bfStack, stackSize);
+
+        if(!tmp) {
+            status = 2;
+            return;
+        }
+
+        memset((tmp + stackSize) - BF_STACK_STEP_SIZE, 0x00, BF_STACK_STEP_SIZE);
+        bfStack = (uint8_t*)tmp;
+    };
+    if(stackPtr > stackSizeReal) {
+        stackSizeReal = stackPtr;
+    }
+}
+
+void lShift() {
+    runOpCount++;
+    stackPtr--;
+    if(!validateStackPtr()) {
+        status = 2;
+        return;
+    }
+}
+
+void inc() {
+    runOpCount++;
+    if(!validateStackPtr()) {
+        status = 2;
+        return;
+    }
+    bfStack[stackPtr]++;
+}
+
+void dec() {
+    runOpCount++;
+    if(!validateStackPtr()) {
+        status = 2;
+        return;
+    }
+    bfStack[stackPtr]--;
+}
+
+void print() {
+    runOpCount++;
+    wOutput[wOutputPtr] = bfStack[stackPtr];
+    wOutputPtr++;
+    if(wOutputPtr > (BF_OUTPUT_SIZE - 1)) {
+        wOutputPtr = 0;
+    }
+}
+
+void input() {
+    runOpCount++;
+
+    bfStack[stackPtr] = (uint8_t)wInput[wInputPtr];
+    if(wInput[wInputPtr] == 0x00 || wInputPtr >= 64) {
+        wInputPtr = 0;
+    } else {
+        wInputPtr++;
+    }
+}
+
+void loop() {
+    runOpCount++;
+    if(bfStack[stackPtr] == 0) {
+        int loopCount = 1;
+        while(loopCount > 0) {
+            instPtr++;
+            if(!validateInstPtr()) {
+                status = 2;
+                return;
+            }
+            if(inst[instPtr] == '[') {
+                loopCount++;
+            } else if(inst[instPtr] == ']') {
+                loopCount--;
+            }
+        }
+    }
+}
+
+void endLoop() {
+    runOpCount++;
+    if(bfStack[stackPtr] != 0) {
+        int loopCount = 1;
+        while(loopCount > 0) {
+            instPtr--;
+            if(!validateInstPtr()) {
+                status = 2;
+                return;
+            }
+            if(inst[instPtr] == ']') {
+                loopCount++;
+            } else if(inst[instPtr] == '[') {
+                loopCount--;
+            }
+        }
+    }
+}
+
+static const NotificationSequence led_on = {
+    &message_blue_255,
+    &message_do_not_reset,
+    NULL,
+};
+
+static const NotificationSequence led_off = {
+    &message_blue_0,
+    NULL,
+};
+
+void input_kill(void* _ctx) {
+    UNUSED(_ctx);
+    killswitch = true;
+}
+
+void beginWorker() {
+    status = 1;
+
+    //redefined from furi_hal_resources.c
+    const GpioPin gpio_button_back = {.port = GPIOC, .pin = LL_GPIO_PIN_13};
+
+    while(inst[instPtr] != 0x00) {
+        if(runOpCount % 500 == 0) {
+            text_box_set_text(wrkrApp->text_box, workerGetOutput());
+            notification_message(wrkrApp->notifications, &led_on);
+        }
+
+        //status 2 indicates failure
+        if(status == 2) {
+            status = 0;
+            break;
+        }
+
+        //read back button directly to avoid weirdness in furi
+        if(killswitch || !furi_hal_gpio_read(&gpio_button_back)) {
+            status = 0;
+            killswitch = false;
+            break;
+        }
+
+        switch(inst[instPtr]) {
+        case '>':
+            rShift();
+            break;
+        case '<':
+            lShift();
+            break;
+
+        case '+':
+            inc();
+            break;
+
+        case '-':
+            dec();
+            break;
+
+        case '.':
+            print();
+            break;
+
+        case ',':
+            input();
+            break;
+
+        case '[':
+            loop();
+            break;
+
+        case ']':
+            endLoop();
+            break;
+
+        default:
+            break;
+        }
+        instPtr++;
+        if(!validateInstPtr()) {
+            status = 0;
+            break;
+        }
+    }
+
+    notification_message(wrkrApp->notifications, &led_off);
+    text_box_set_text(wrkrApp->text_box, workerGetOutput());
+    status = 0;
+}

+ 11 - 0
brainfuck/worker.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#include "brainfuck_i.h"
+
+void initWorker(BFApp* application);
+char* workerGetOutput();
+int getStackSize();
+int getOpCount();
+int getStatus();
+void beginWorker();
+void killThread();