feat: Added save editor mode

This commit is contained in:
WerWolv
2025-08-03 13:46:33 +02:00
parent 56d1e2670e
commit ea072296cf
8 changed files with 271 additions and 3 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -28,6 +28,7 @@ namespace hex::plugin::builtin {
void handleSettingsResetCommand(const std::vector<std::string> &args);
void handleDebugModeCommand(const std::vector<std::string> &args);
void handleValidatePluginCommand(const std::vector<std::string> &args);
void handleSaveEditorCommand(const std::vector<std::string> &args);
void registerCommandForwarders();

View File

@@ -54,10 +54,10 @@ namespace hex::plugin::builtin {
[[nodiscard]] std::pair<Region, bool> getRegionValidity(u64 address) const override;
private:
void convertToMemoryFile();
void convertToDirectAccess();
private:
void handleFileChange();
bool open(bool memoryMapped);

View File

@@ -0,0 +1,32 @@
#pragma once
#include <hex/ui/view.hpp>
#include <content/providers/file_provider.hpp>
#include <pl/pattern_language.hpp>
#include <ui/pattern_value_editor.hpp>
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<std::string> m_saveEditorAuthors;
std::vector<std::string> m_saveEditorDescriptions;
};
}

View File

@@ -1,5 +1,4 @@
#include <content/command_line_interface.hpp>
#include <content/providers/file_provider.hpp>
#include <hex/api/content_registry.hpp>
#include <hex/api/imhex_api.hpp>
@@ -15,6 +14,7 @@
#include <hex/helpers/utils.hpp>
#include <hex/helpers/default_paths.hpp>
#include <hex/helpers/debugging.hpp>
#include <hex/helpers/http_requests.hpp>
#include <hex/subcommands/subcommands.hpp>
#include <hex/trace/stacktrace.hpp>
@@ -25,6 +25,9 @@
#include <pl/cli/cli.hpp>
#include <content/providers/file_provider.hpp>
#include <content/views/fullscreen/view_fullscreen_save_editor.hpp>
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<std::string> &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] <file path|gist id>");
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<ViewFullScreenSaveEditor>(saveEditorSourceCode);
}
void registerCommandForwarders() {
hex::subcommands::registerSubCommand("open", [](const std::vector<std::string> &args){

View File

@@ -0,0 +1,160 @@
#include <content/views/fullscreen/view_fullscreen_save_editor.hpp>
#include <fonts/vscode_icons.hpp>
#include <hex/api/content_registry.hpp>
#include <toasts/toast_notification.hpp>
#include <wolv/utils/lock.hpp>
#include <pl/patterns/pattern.hpp>
#include <popups/popup_question.hpp>
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<pl::ptrn::Pattern*> 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();
}
}
}
}
}

View File

@@ -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") {