From ea072296cf7ee27b9530284206e315a452ef2484 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Sun, 3 Aug 2025 13:46:33 +0200 Subject: [PATCH] feat: Added save editor mode --- dist/web/source/wasm-config.js | 4 + plugins/builtin/CMakeLists.txt | 2 + .../content/command_line_interface.hpp | 1 + .../content/providers/file_provider.hpp | 2 +- .../view_fullscreen_save_editor.hpp | 32 ++++ .../source/content/command_line_interface.cpp | 70 +++++++- .../view_fullscreen_save_editor.cpp | 160 ++++++++++++++++++ plugins/builtin/source/plugin_builtin.cpp | 3 +- 8 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 plugins/builtin/include/content/views/fullscreen/view_fullscreen_save_editor.hpp create mode 100644 plugins/builtin/source/content/views/fullscreen/view_fullscreen_save_editor.cpp diff --git a/dist/web/source/wasm-config.js b/dist/web/source/wasm-config.js index 22632b890..b16a5fe29 100644 --- a/dist/web/source/wasm-config.js +++ b/dist/web/source/wasm-config.js @@ -251,6 +251,10 @@ const urlParams = new URLSearchParams(queryString); if (urlParams.has("lang")) { Module["arguments"].push("--language"); Module["arguments"].push(urlParams.get("lang")); +} else if (urlParams.has("save-editor")) { + Module["arguments"].push("--save-editor"); + Module["arguments"].push("gist"); + Module["arguments"].push(urlParams.get("save-editor")); } window.addEventListener('resize', js_resizeCanvas, false); diff --git a/plugins/builtin/CMakeLists.txt b/plugins/builtin/CMakeLists.txt index 29a566d67..b9ca0a149 100644 --- a/plugins/builtin/CMakeLists.txt +++ b/plugins/builtin/CMakeLists.txt @@ -118,6 +118,8 @@ add_imhex_plugin( source/content/views/view_highlight_rules.cpp source/content/views/view_tutorials.cpp + source/content/views/fullscreen/view_fullscreen_save_editor.cpp + source/content/text_highlighting/pattern_language.cpp INCLUDES include diff --git a/plugins/builtin/include/content/command_line_interface.hpp b/plugins/builtin/include/content/command_line_interface.hpp index 88d581a3e..d21a0687b 100644 --- a/plugins/builtin/include/content/command_line_interface.hpp +++ b/plugins/builtin/include/content/command_line_interface.hpp @@ -28,6 +28,7 @@ namespace hex::plugin::builtin { void handleSettingsResetCommand(const std::vector &args); void handleDebugModeCommand(const std::vector &args); void handleValidatePluginCommand(const std::vector &args); + void handleSaveEditorCommand(const std::vector &args); void registerCommandForwarders(); diff --git a/plugins/builtin/include/content/providers/file_provider.hpp b/plugins/builtin/include/content/providers/file_provider.hpp index e224e2ab5..1c86ca3a2 100644 --- a/plugins/builtin/include/content/providers/file_provider.hpp +++ b/plugins/builtin/include/content/providers/file_provider.hpp @@ -54,10 +54,10 @@ namespace hex::plugin::builtin { [[nodiscard]] std::pair getRegionValidity(u64 address) const override; - private: void convertToMemoryFile(); void convertToDirectAccess(); + private: void handleFileChange(); bool open(bool memoryMapped); diff --git a/plugins/builtin/include/content/views/fullscreen/view_fullscreen_save_editor.hpp b/plugins/builtin/include/content/views/fullscreen/view_fullscreen_save_editor.hpp new file mode 100644 index 000000000..0f229f05d --- /dev/null +++ b/plugins/builtin/include/content/views/fullscreen/view_fullscreen_save_editor.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +namespace hex::plugin::builtin { + + class ViewFullScreenSaveEditor : public View::FullScreen { + public: + explicit ViewFullScreenSaveEditor(std::string sourceCode); + + void drawContent() override; + + private: + void drawFileSelectScreen(); + void drawSaveEditorScreen(); + + private: + std::string m_sourceCode; + FileProvider m_provider; + + pl::PatternLanguage m_runtime; + ui::PatternValueEditor m_saveEditor; + + std::string m_saveEditorName; + std::vector m_saveEditorAuthors; + std::vector m_saveEditorDescriptions; + }; + +} diff --git a/plugins/builtin/source/content/command_line_interface.cpp b/plugins/builtin/source/content/command_line_interface.cpp index 0d39973b1..ce87abcf1 100644 --- a/plugins/builtin/source/content/command_line_interface.cpp +++ b/plugins/builtin/source/content/command_line_interface.cpp @@ -1,5 +1,4 @@ #include -#include #include #include @@ -15,6 +14,7 @@ #include #include #include +#include #include #include @@ -25,6 +25,9 @@ #include +#include +#include + namespace hex::plugin::builtin { using namespace hex::literals; @@ -447,6 +450,71 @@ namespace hex::plugin::builtin { std::exit(EXIT_SUCCESS); } + void handleSaveEditorCommand(const std::vector &args) { + std::string type; + std::string argument; + if (args.size() == 1) { + type = "file"; + argument = args[0]; + } else if (args.size() == 2) { + type = args[0]; + argument = args[1]; + } else { + log::println("usage: imhex --save-editor [file|gist] "); + std::exit(EXIT_FAILURE); + } + + std::string saveEditorSourceCode; + if (type == "file") { + if (!wolv::io::fs::exists(argument)) { + log::println("Save Editor file '{}' does not exist!", argument); + std::exit(EXIT_FAILURE); + } + + wolv::io::File file(argument, wolv::io::File::Mode::Read); + if (!file.isValid()) { + log::println("Failed to open Save Editor file '{}'", argument); + std::exit(EXIT_FAILURE); + } + saveEditorSourceCode = file.readString(); + } else if (type == "gist") { + HttpRequest request("GET", "https://api.github.com/gists/" + argument); + const auto response = request.execute().get(); + if (!response.isSuccess()) { + switch (response.getStatusCode()) { + case 404: + log::println("Gist with ID '{}' not found!", argument); + break; + case 403: + log::println("Gist with ID '{}' is private or you have exceeded the rate limit!", argument); + break; + default: + log::println("Failed to fetch Gist with ID '{}': {}", argument, response.getStatusCode()); + break; + } + std::exit(EXIT_FAILURE); + } + + try { + const auto json = nlohmann::json::parse(response.getData()); + if (!json.contains("files") || json["files"].size() != 1) { + log::println("Gist with ID '{}' does not have exactly one file!", argument); + std::exit(EXIT_FAILURE); + } + + saveEditorSourceCode = (*json["files"].begin())["content"]; + } catch (const nlohmann::json::parse_error &e) { + log::println("Failed to parse Gist response: {}", e.what()); + std::exit(EXIT_FAILURE); + } + } else { + log::println("Unknown source type '{}'. Use 'file' or 'gist'.", type); + std::exit(EXIT_FAILURE); + } + + ContentRegistry::Views::setFullScreenView(saveEditorSourceCode); + } + void registerCommandForwarders() { hex::subcommands::registerSubCommand("open", [](const std::vector &args){ diff --git a/plugins/builtin/source/content/views/fullscreen/view_fullscreen_save_editor.cpp b/plugins/builtin/source/content/views/fullscreen/view_fullscreen_save_editor.cpp new file mode 100644 index 000000000..492679296 --- /dev/null +++ b/plugins/builtin/source/content/views/fullscreen/view_fullscreen_save_editor.cpp @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace hex::plugin::builtin { + + ViewFullScreenSaveEditor::ViewFullScreenSaveEditor(std::string sourceCode) : m_sourceCode(std::move(sourceCode)) { + m_runtime.addPragma("name", [this](auto &, const std::string &value) -> bool { + m_saveEditorName = wolv::util::trim(value); + return true; + }); + + m_runtime.addPragma("author", [this](auto &, const std::string &value) -> bool { + m_saveEditorAuthors.push_back(wolv::util::trim(value)); + return true; + }); + + m_runtime.addPragma("description", [this](auto &, const std::string &value) -> bool { + m_saveEditorDescriptions.push_back(wolv::util::trim(value)); + return true; + }); + + std::ignore = m_runtime.parseString(m_sourceCode, pl::api::Source::DefaultSource); + } + + + void ViewFullScreenSaveEditor::drawContent() { + if (!m_provider.isReadable()) { + drawFileSelectScreen(); + } else { + drawSaveEditorScreen(); + } + } + + void ViewFullScreenSaveEditor::drawFileSelectScreen() { + const auto windowSize = ImGui::GetWindowSize(); + const auto optionsWindowSize = ImVec2(windowSize.x * 2 / 3, 0); + ImGui::NewLine(); + ImGui::SetCursorPosX((windowSize.x - optionsWindowSize.x) / 2.0F); + if (ImGuiExt::BeginSubWindow(m_saveEditorName.empty() ? "Save Editor" : m_saveEditorName.c_str(), nullptr, optionsWindowSize)) { + for (const auto &author : m_saveEditorAuthors) { + ImGui::TextUnformatted(author.c_str()); + ImGui::SameLine(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + ImGui::SameLine(); + } + ImGui::NewLine(); + ImGui::Separator(); + ImGui::NewLine(); + + for (const auto &description : m_saveEditorDescriptions) { + ImGui::TextWrapped("%s", description.c_str()); + ImGui::NewLine(); + } + + ImGui::NewLine(); + + if (ImGuiExt::DimmedButton(hex::format("{} {}", ICON_VS_OPEN_PREVIEW, "Select Save File").c_str(), ImVec2(-1, 0))) { + fs::openFileBrowser(fs::DialogMode::Open, {}, [this](const std::fs::path &path) { + this->m_provider.setPath(path); + if (!this->m_provider.open()) { + ui::ToastError::open("The selected file could not be opened. Please ensure the file exists and is readable."); + } + this->m_provider.convertToMemoryFile(); + + ContentRegistry::PatternLanguage::configureRuntime(m_runtime, &m_provider); + if (!m_runtime.executeString(this->m_sourceCode)) { + ui::ToastError::open("Failed to execute the save editor script. Please check the log for errors."); + for (const auto &error : m_runtime.getCompileErrors()) { + log::error("Save Editor Error: {}", error.format()); + } + + if (const auto &error = m_runtime.getEvalError(); error.has_value()) { + log::error("Evaluation Error: {}:{} | {}", error->line, error->column, error->message); + } + } + }); + } + + ImGuiExt::EndSubWindow(); + } + } + + void ViewFullScreenSaveEditor::drawSaveEditorScreen() { + constexpr static auto SimplifiedEditorAttribute = "hex::editor_export"; + if (TRY_LOCK(ContentRegistry::PatternLanguage::getRuntimeLock()) && m_runtime.arePatternsValid()) { + const auto &patternSet = m_runtime.getPatternsWithAttribute(SimplifiedEditorAttribute); + std::vector patterns = { patternSet.begin(), patternSet.end() }; + std::ranges::sort(patterns, [](const pl::ptrn::Pattern *a, const pl::ptrn::Pattern *b) { + return a->getOffset() < b->getOffset() || a->getDisplayName() < b->getDisplayName(); + }); + + ImGui::SameLine(ImGui::GetWindowSize().x - 75_scaled); + if (ImGuiExt::DimmedIconButton(ICON_VS_SAVE_AS, ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_ToolbarBlue))) { + fs::openFileBrowser(fs::DialogMode::Save, {}, [this](const std::fs::path &path) { + this->m_provider.saveAs(path); + this->m_provider.close(); + this->m_runtime.reset(); + }); + } + ImGui::SameLine(); + if (ImGuiExt::DimmedIconButton(ICON_VS_CLOSE, ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_ToolbarRed))) { + ui::PopupQuestion::open("Do you want to close the Save Editor?", [this] { + this->m_provider.close(); + }, []{}); + } + + if (!patterns.empty()) { + if (ImGui::BeginChild("##editor")) { + for (const auto &pattern : patterns) { + ImGui::PushID(pattern); + try { + const auto attribute = pattern->getAttributeArguments(SimplifiedEditorAttribute); + + const auto name = attribute.size() >= 1 ? attribute[0].toString() : pattern->getDisplayName(); + const auto description = attribute.size() >= 2 ? attribute[1].toString() : pattern->getComment(); + + const auto widgetPos = 200_scaled; + ImGui::TextUnformatted(name.c_str()); + ImGui::SameLine(0, 20_scaled); + if (ImGui::GetCursorPosX() < widgetPos) + ImGui::SetCursorPosX(widgetPos); + + ImGui::PushStyleVarY(ImGuiStyleVar_FramePadding, 0); + ImGui::PushItemWidth(-50_scaled); + pattern->accept(m_saveEditor); + ImGui::PopItemWidth(); + ImGui::PopStyleVar(); + + if (!description.empty()) { + ImGui::PushFont(nullptr, ImGui::GetFontSize() * 0.8F); + ImGui::BeginDisabled(); + ImGui::Indent(); + ImGui::TextWrapped("%s", description.c_str()); + ImGui::Unindent(); + ImGui::EndDisabled(); + ImGui::PopFont(); + } + + ImGui::Separator(); + + } catch (const std::exception &e) { + ImGui::TextUnformatted(pattern->getDisplayName().c_str()); + ImGui::TextUnformatted(e.what()); + } + + ImGui::PopID(); + } + + ImGui::EndChild(); + } + } + } + } + +} diff --git a/plugins/builtin/source/plugin_builtin.cpp b/plugins/builtin/source/plugin_builtin.cpp index 983f2c438..6f26dd28a 100644 --- a/plugins/builtin/source/plugin_builtin.cpp +++ b/plugins/builtin/source/plugin_builtin.cpp @@ -84,7 +84,8 @@ IMHEX_PLUGIN_SUBCOMMANDS() { { "demangle", "", "Demangle a mangled symbol", hex::plugin::builtin::handleDemangleCommand }, { "reset-settings", "", "Resets all settings back to default", hex::plugin::builtin::handleSettingsResetCommand }, { "debug-mode", "", "Enables debugging features", hex::plugin::builtin::handleDebugModeCommand, }, - { "validate-plugin", "", "Validates that a plugin can be loaded", hex::plugin::builtin::handleValidatePluginCommand } + { "validate-plugin", "", "Validates that a plugin can be loaded", hex::plugin::builtin::handleValidatePluginCommand }, + { "save-editor", "", "Opens a pattern file for save file editing", hex::plugin::builtin::handleSaveEditorCommand }, }; IMHEX_PLUGIN_SETUP("Built-in", "WerWolv", "Default ImHex functionality") {