diff --git a/.gitignore b/.gitignore index 5811c09..be58bda 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ Release/ *.dll hyperion_layout.ini imgui.ini +CONTRIBUTING.md +SKILL.md \ No newline at end of file diff --git a/src/scripting/lua_engine.cpp b/src/scripting/lua_engine.cpp index 81f8fe0..31a31ad 100644 --- a/src/scripting/lua_engine.cpp +++ b/src/scripting/lua_engine.cpp @@ -1,7 +1,9 @@ #include "lua_engine.h" #include #include +#include #include +#include extern "C" { #include @@ -13,6 +15,10 @@ namespace hype { namespace { +// ----------------------------------------------------------------------- +// Registry helpers +// ----------------------------------------------------------------------- + LuaEngine* get_engine(lua_State* L) { lua_getfield(L, LUA_REGISTRYINDEX, "__hype_engine"); auto* eng = static_cast(lua_touserdata(L, -1)); @@ -34,6 +40,76 @@ PEImage* get_img(lua_State* L) { return img; } +// ----------------------------------------------------------------------- +// Hotkey parsing helpers +// ----------------------------------------------------------------------- + +struct HotkeySpec { + ImGuiKey key = ImGuiKey_None; + bool ctrl = false; + bool shift = false; + bool alt = false; +}; + +// Parse strings like "Ctrl+Shift+R", "Alt+F7", "F5", "Ctrl+G" +HotkeySpec parse_hotkey(const std::string& s) { + HotkeySpec spec; + std::string token; + std::string src = s; + // Split on '+' + std::vector parts; + size_t pos = 0; + while ((pos = src.find('+')) != std::string::npos) { + parts.push_back(src.substr(0, pos)); + src.erase(0, pos + 1); + } + parts.push_back(src); // last part + + for (auto& p : parts) { + // to lower for comparison + std::string lp = p; + for (auto& c : lp) c = static_cast(std::tolower(static_cast(c))); + if (lp == "ctrl") { spec.ctrl = true; continue; } + if (lp == "shift") { spec.shift = true; continue; } + if (lp == "alt") { spec.alt = true; continue; } + + // Match ImGuiKey + if (p.size() == 1) { + char c = static_cast(std::toupper(static_cast(p[0]))); + if (c >= 'A' && c <= 'Z') + spec.key = static_cast(ImGuiKey_A + (c - 'A')); + else if (c >= '0' && c <= '9') + spec.key = static_cast(ImGuiKey_0 + (c - '0')); + } else if (lp[0] == 'f' && lp.size() <= 3) { + // F1–F12 + int n = 0; + try { n = std::stoi(lp.substr(1)); } catch (...) {} + if (n >= 1 && n <= 12) + spec.key = static_cast(ImGuiKey_F1 + (n - 1)); + } else if (lp == "space") { spec.key = ImGuiKey_Space; } + else if (lp == "tab") { spec.key = ImGuiKey_Tab; } + else if (lp == "enter") { spec.key = ImGuiKey_Enter; } + else if (lp == "escape") { spec.key = ImGuiKey_Escape; } + else if (lp == "delete") { spec.key = ImGuiKey_Delete; } + else if (lp == "home") { spec.key = ImGuiKey_Home; } + else if (lp == "end") { spec.key = ImGuiKey_End; } + } + return spec; +} + +bool check_hotkey(const HotkeySpec& spec) { + if (spec.key == ImGuiKey_None) return false; + auto& io = ImGui::GetIO(); + if (spec.ctrl != io.KeyCtrl) return false; + if (spec.shift != io.KeyShift) return false; + if (spec.alt != io.KeyAlt) return false; + return ImGui::IsKeyPressed(spec.key); +} + +// ----------------------------------------------------------------------- +// Existing Lua API +// ----------------------------------------------------------------------- + int l_get_name(lua_State* L) { auto* db = get_db(L); if (!db) { lua_pushnil(L); return 1; } @@ -102,9 +178,9 @@ int l_get_bytes(lua_State* L) { for (auto& seg : img->segments) { if (!seg.contains(addr)) continue; - size_t off = static_cast(addr - seg.va); + size_t off = static_cast(addr - seg.va); size_t avail = seg.data.size() - off; - size_t n = static_cast(len) < avail ? static_cast(len) : avail; + size_t n = static_cast(len) < avail ? static_cast(len) : avail; lua_pushlstring(L, reinterpret_cast(seg.data.data() + off), n); return 1; } @@ -178,7 +254,234 @@ int l_goto(lua_State* L) { return 0; } -} // anon +int l_get_comment(lua_State* L) { + auto* db = get_db(L); + if (!db) { lua_pushstring(L, ""); return 1; } + va_t addr = static_cast(luaL_checkinteger(L, 1)); + auto it = db->comments.find(addr); + if (it != db->comments.end()) + lua_pushstring(L, it->second.c_str()); + else + lua_pushstring(L, ""); + return 1; +} + +int l_get_string(lua_State* L) { + auto* db = get_db(L); + if (!db) { lua_pushnil(L); return 1; } + va_t addr = static_cast(luaL_checkinteger(L, 1)); + for (auto& [sa, ss] : db->strings) { + if (sa == addr) { + lua_pushstring(L, ss.c_str()); + return 1; + } + } + lua_pushnil(L); + return 1; +} + +int l_get_image_base(lua_State* L) { + auto* db = get_db(L); + if (!db) { lua_pushinteger(L, 0); return 1; } + lua_pushinteger(L, static_cast(db->image_base)); + return 1; +} + +int l_get_arch(lua_State* L) { + auto* img = get_img(L); + if (!img) { lua_pushstring(L, "unknown"); return 1; } + switch (img->arch) { + case Arch::X86: lua_pushstring(L, "x86"); break; + case Arch::X64: lua_pushstring(L, "x64"); break; + case Arch::ARM: lua_pushstring(L, "arm"); break; + case Arch::ARM64: lua_pushstring(L, "arm64"); break; + case Arch::MIPS: lua_pushstring(L, "mips"); break; + case Arch::PPC: lua_pushstring(L, "ppc"); break; + default: lua_pushstring(L, "unknown"); break; + } + return 1; +} + +int l_get_segments(lua_State* L) { + auto* img = get_img(L); + if (!img) { lua_newtable(L); return 1; } + lua_newtable(L); + int idx = 1; + for (auto& seg : img->segments) { + lua_newtable(L); + lua_pushstring(L, seg.name.c_str()); + lua_setfield(L, -2, "name"); + lua_pushinteger(L, static_cast(seg.va)); + lua_setfield(L, -2, "addr"); + lua_pushinteger(L, static_cast(seg.data.size())); + lua_setfield(L, -2, "size"); + lua_pushinteger(L, static_cast(seg.flags)); + lua_setfield(L, -2, "flags"); + lua_rawseti(L, -2, idx++); + } + return 1; +} + +int l_get_cursor(lua_State* L) { + // The cursor is held by DisasmView; we expose it via a registry pointer set by App + lua_getfield(L, LUA_REGISTRYINDEX, "__hype_cursor"); + auto* cur = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + if (!cur) { lua_pushinteger(L, 0); return 1; } + lua_pushinteger(L, static_cast(*cur)); + return 1; +} + +int l_create_function(lua_State* L) { + auto* db = get_db(L); + if (!db) return 0; + va_t addr = static_cast(luaL_checkinteger(L, 1)); + std::lock_guard lk(db->mtx); + if (!db->funcs.count(addr)) { + Function f; + f.entry = addr; + f.name = fmt::format("sub_{:X}", addr - db->image_base); + db->funcs[addr] = std::move(f); + db->names[addr] = db->funcs[addr].name; + } + return 0; +} + +// ----------------------------------------------------------------------- +// open_results(title, headers_table, rows_table) +// headers_table : {"Col1", "Col2", ...} +// rows_table : { {addr=0x..., cols={"v1","v2",...}}, ... } +// ----------------------------------------------------------------------- +int l_open_results(lua_State* L) { + auto* eng = get_engine(L); + if (!eng) return 0; + + const char* title = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TTABLE); + luaL_checktype(L, 3, LUA_TTABLE); + + ResultsWindow w; + w.title = title; + + // Read headers + int nhdr = static_cast(lua_rawlen(L, 2)); + for (int i = 1; i <= nhdr; ++i) { + lua_rawgeti(L, 2, i); + const char* s = lua_tostring(L, -1); + w.headers.push_back(s ? s : ""); + lua_pop(L, 1); + } + + // Read rows + int nrows = static_cast(lua_rawlen(L, 3)); + for (int i = 1; i <= nrows; ++i) { + lua_rawgeti(L, 3, i); // push row table + if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; } + + ResultsRow row; + + lua_getfield(L, -1, "addr"); + row.addr = static_cast(lua_tointeger(L, -1)); + lua_pop(L, 1); + + lua_getfield(L, -1, "cols"); // push cols table + if (lua_istable(L, -1)) { + int ncols = static_cast(lua_rawlen(L, -1)); + for (int c = 1; c <= ncols; ++c) { + lua_rawgeti(L, -1, c); + const char* s = lua_tostring(L, -1); + row.cols.push_back(s ? s : ""); + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop cols table + lua_pop(L, 1); // pop row table + + w.rows.push_back(std::move(row)); + } + + eng->push_result_window(std::move(w)); + return 0; +} + +// ----------------------------------------------------------------------- +// Plugin registration Lua API +// ----------------------------------------------------------------------- + +int l_register_plugin(lua_State* L) { + auto* eng = get_engine(L); + if (!eng) return 0; + const char* name = luaL_optstring(L, 1, "Unnamed Plugin"); + const char* desc = luaL_optstring(L, 2, ""); + + // Find the entry that was pre-created for the current file, or create one + auto& plugins = const_cast&>(eng->plugins()); + for (auto& p : plugins) { + if (p.path == eng->current_plugin_name()) { + p.name = name; + p.desc = desc; + return 0; + } + } + // Fallback: shouldn't normally reach here + PluginEntry e; + e.name = name; + e.desc = desc; + e.path = eng->current_plugin_name(); + plugins.push_back(std::move(e)); + return 0; +} + +int l_register_menu_item(lua_State* L) { + auto* eng = get_engine(L); + if (!eng) return 0; + + const char* label = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + lua_pushvalue(L, 2); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + + auto& plugins = const_cast&>(eng->plugins()); + // Add to the most recently active plugin + for (auto it = plugins.rbegin(); it != plugins.rend(); ++it) { + if (it->path == eng->current_plugin_name()) { + it->items.push_back({label, ref}); + return 0; + } + } + // If no plugin registered yet, attach to a default entry + if (!plugins.empty()) { + plugins.back().items.push_back({label, ref}); + } + return 0; +} + +int l_register_hotkey(lua_State* L) { + auto* eng = get_engine(L); + if (!eng) return 0; + const char* key_str = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + lua_pushvalue(L, 2); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + eng->add_hotkey(key_str, ref); + return 0; +} + +int l_register_on_analysis_complete(lua_State* L) { + auto* eng = get_engine(L); + if (!eng) return 0; + luaL_checktype(L, 1, LUA_TFUNCTION); + lua_pushvalue(L, 1); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + eng->add_analysis_cb(ref); + return 0; +} + +} // anon namespace + +// ----------------------------------------------------------------------- +// LuaEngine implementation +// ----------------------------------------------------------------------- LuaEngine::LuaEngine() { L_ = luaL_newstate(); @@ -186,11 +489,19 @@ LuaEngine::LuaEngine() { } LuaEngine::~LuaEngine() { + // Release all Lua registry refs before closing + for (auto& p : plugins_) + for (auto& item : p.items) + if (item.cb_ref != LUA_NOREF) luaL_unref(L_, LUA_REGISTRYINDEX, item.cb_ref); + for (auto& [_, ref] : hotkeys_) + if (ref != LUA_NOREF) luaL_unref(L_, LUA_REGISTRYINDEX, ref); + for (auto ref : on_analysis_cbs_) + if (ref != LUA_NOREF) luaL_unref(L_, LUA_REGISTRYINDEX, ref); if (L_) lua_close(L_); } void LuaEngine::init(AnalysisDB* db, PEImage* img) { - db_ = db; + db_ = db; img_ = img; register_api(); } @@ -207,16 +518,157 @@ void LuaEngine::register_api() { lua_pushlightuserdata(L_, &nav_cb_); lua_setfield(L_, LUA_REGISTRYINDEX, "__hype_nav"); - lua_register(L_, "get_name", l_get_name); - lua_register(L_, "set_name", l_set_name); - lua_register(L_, "get_func", l_get_func); - lua_register(L_, "get_insn", l_get_insn); - lua_register(L_, "get_bytes", l_get_bytes); - lua_register(L_, "set_comment", l_set_comment); - lua_register(L_, "get_xrefs_to", l_get_xrefs_to); - lua_register(L_, "get_functions", l_get_functions); - lua_register(L_, "print", l_print); - lua_register(L_, "goto_addr", l_goto); + // Core API + lua_register(L_, "get_name", l_get_name); + lua_register(L_, "set_name", l_set_name); + lua_register(L_, "get_func", l_get_func); + lua_register(L_, "get_insn", l_get_insn); + lua_register(L_, "get_bytes", l_get_bytes); + lua_register(L_, "set_comment", l_set_comment); + lua_register(L_, "get_comment", l_get_comment); + lua_register(L_, "get_xrefs_to", l_get_xrefs_to); + lua_register(L_, "get_functions", l_get_functions); + lua_register(L_, "print", l_print); + lua_register(L_, "goto_addr", l_goto); + lua_register(L_, "get_string", l_get_string); + lua_register(L_, "get_image_base", l_get_image_base); + lua_register(L_, "get_arch", l_get_arch); + lua_register(L_, "get_segments", l_get_segments); + lua_register(L_, "get_cursor", l_get_cursor); + lua_register(L_, "create_function",l_create_function); + lua_register(L_, "open_results", l_open_results); + + // Plugin registration API + lua_register(L_, "register_plugin", l_register_plugin); + lua_register(L_, "register_menu_item", l_register_menu_item); + lua_register(L_, "register_hotkey", l_register_hotkey); + lua_register(L_, "register_on_analysis_complete", l_register_on_analysis_complete); +} + +// Register engine pointer and api before analysis (plugins load pre-loop). +// We call register_api early so plugins can call register_* at load time +// even though db_ and img_ are still null — API functions guard for null. +void LuaEngine::load_plugins(const std::filesystem::path& dir) { + // Register the engine pointer early so register_* functions work + lua_pushlightuserdata(L_, this); + lua_setfield(L_, LUA_REGISTRYINDEX, "__hype_engine"); + lua_pushlightuserdata(L_, &output_); + lua_setfield(L_, LUA_REGISTRYINDEX, "__hype_output"); + + // Register plugin API functions (safe even with null db_/img_) + lua_register(L_, "get_name", l_get_name); + lua_register(L_, "set_name", l_set_name); + lua_register(L_, "get_func", l_get_func); + lua_register(L_, "get_insn", l_get_insn); + lua_register(L_, "get_bytes", l_get_bytes); + lua_register(L_, "set_comment", l_set_comment); + lua_register(L_, "get_comment", l_get_comment); + lua_register(L_, "get_xrefs_to", l_get_xrefs_to); + lua_register(L_, "get_functions", l_get_functions); + lua_register(L_, "print", l_print); + lua_register(L_, "goto_addr", l_goto); + lua_register(L_, "get_string", l_get_string); + lua_register(L_, "get_image_base", l_get_image_base); + lua_register(L_, "get_arch", l_get_arch); + lua_register(L_, "get_segments", l_get_segments); + lua_register(L_, "get_cursor", l_get_cursor); + lua_register(L_, "create_function",l_create_function); + lua_register(L_, "open_results", l_open_results); + lua_register(L_, "register_plugin", l_register_plugin); + lua_register(L_, "register_menu_item", l_register_menu_item); + lua_register(L_, "register_hotkey", l_register_hotkey); + lua_register(L_, "register_on_analysis_complete", l_register_on_analysis_complete); + + if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) { + spdlog::info("lua_engine: plugins dir not found at {}", dir.string()); + return; + } + + // Collect and sort .lua files alphabetically + std::vector files; + for (auto& entry : std::filesystem::directory_iterator(dir)) { + if (entry.path().extension() == ".lua") + files.push_back(entry.path()); + } + std::sort(files.begin(), files.end()); + + for (auto& fp : files) { + PluginEntry pe; + pe.path = fp.string(); + pe.name = fp.stem().string(); // default name = filename stem + pe.desc = ""; + + current_plugin_ = pe.path; + plugins_.push_back(std::move(pe)); + + output_.clear(); + int err = luaL_dofile(L_, fp.string().c_str()); + if (err) { + const char* msg = lua_tostring(L_, -1); + plugins_.back().error = true; + plugins_.back().error_msg = msg ? msg : "unknown error"; + lua_pop(L_, 1); + spdlog::warn("lua_engine: plugin error [{}]: {}", fp.filename().string(), plugins_.back().error_msg); + } else { + spdlog::info("lua_engine: loaded plugin [{}] -> \"{}\"", + fp.filename().string(), plugins_.back().name); + } + } + + spdlog::info("lua_engine: {} plugin(s) loaded from {}", plugins_.size(), dir.string()); +} + +void LuaEngine::invoke_menu_item(int plugin_idx, int item_idx) { + if (plugin_idx < 0 || plugin_idx >= static_cast(plugins_.size())) return; + auto& plugin = plugins_[static_cast(plugin_idx)]; + if (item_idx < 0 || item_idx >= static_cast(plugin.items.size())) return; + int ref = plugin.items[static_cast(item_idx)].cb_ref; + if (ref == LUA_NOREF) return; + + output_.clear(); + lua_rawgeti(L_, LUA_REGISTRYINDEX, ref); + int err = lua_pcall(L_, 0, 0, 0); + if (err) { + const char* msg = lua_tostring(L_, -1); + output_ += std::string("[plugin error] ") + (msg ? msg : "unknown") + "\n"; + lua_pop(L_, 1); + } + // output_ holds everything l_print wrote during the call. + // The caller (App::render_menubar) reads it via last_output(). +} + +void LuaEngine::run_analysis_complete_callbacks() { + for (int ref : on_analysis_cbs_) { + if (ref == LUA_NOREF) continue; + output_.clear(); + lua_rawgeti(L_, LUA_REGISTRYINDEX, ref); + int err = lua_pcall(L_, 0, 0, 0); + if (err) { + const char* msg = lua_tostring(L_, -1); + spdlog::warn("lua_engine: on_analysis_complete callback error: {}", + msg ? msg : "unknown"); + lua_pop(L_, 1); + } + } +} + +void LuaEngine::check_hotkeys() { + auto& io = ImGui::GetIO(); + if (io.WantTextInput) return; + for (auto& [key_str, ref] : hotkeys_) { + if (ref == LUA_NOREF) continue; + auto spec = parse_hotkey(key_str); + if (!check_hotkey(spec)) continue; + + output_.clear(); + lua_rawgeti(L_, LUA_REGISTRYINDEX, ref); + int err = lua_pcall(L_, 0, 0, 0); + if (err) { + const char* msg = lua_tostring(L_, -1); + spdlog::warn("lua_engine: hotkey callback error: {}", msg ? msg : "unknown"); + lua_pop(L_, 1); + } + } } std::string LuaEngine::execute(const std::string& code) { @@ -231,4 +683,4 @@ std::string LuaEngine::execute(const std::string& code) { return output_; } -} +} // namespace hype diff --git a/src/scripting/lua_engine.h b/src/scripting/lua_engine.h index 15cee9c..aa580d8 100644 --- a/src/scripting/lua_engine.h +++ b/src/scripting/lua_engine.h @@ -2,12 +2,46 @@ #include "core/analysis/analysis_db.h" #include "core/loader/pe_loader.h" #include +#include #include +#include struct lua_State; namespace hype { +// A single action registered by a plugin via register_menu_item(). +struct PluginMenuItem { + std::string label; + int cb_ref = -1; // Lua registry reference (luaL_ref) +}; + +// One loaded plugin file. +struct PluginEntry { + std::string name; // from register_plugin() + std::string desc; // from register_plugin() + std::string path; // filesystem path of the .lua file + std::vector items; + bool error = false; + std::string error_msg; +}; + +// One row in a plugin results window. +struct ResultsRow { + va_t addr; // address to navigate to when clicked + std::vector cols; // column values (must match headers count) +}; + +// A results window opened by a plugin via open_results(). +struct ResultsWindow { + std::string title; + std::vector headers; + std::vector rows; + bool open = true; + char filter[256] = {}; + int selected = -1; +}; + class LuaEngine { public: LuaEngine(); @@ -18,14 +52,45 @@ class LuaEngine { void set_navigate_cb(std::function cb) { nav_cb_ = std::move(cb); } + // Plugin system — public interface used by App + void load_plugins(const std::filesystem::path& dir); + void invoke_menu_item(int plugin_idx, int item_idx); + void run_analysis_complete_callbacks(); + void check_hotkeys(); // call once per frame from App::handle_keys() + + const std::vector& plugins() const { return plugins_; } + std::vector& plugins() { return plugins_; } + + // Results windows — rendered by App each frame + std::vector& result_windows() { return result_windows_; } + const std::vector& result_windows() const { return result_windows_; } + + // Returns whatever l_print accumulated during the last invoke_menu_item call. + const std::string& last_output() const { return output_; } + + // Called by Lua C-function wrappers inside the anonymous namespace + const std::string& current_plugin_name() const { return current_plugin_; } + void add_hotkey(const std::string& key_str, int ref) { hotkeys_.emplace_back(key_str, ref); } + void add_analysis_cb(int ref) { on_analysis_cbs_.push_back(ref); } + void push_result_window(ResultsWindow w) { result_windows_.push_back(std::move(w)); } + private: void register_api(); - lua_State* L_ = nullptr; - AnalysisDB* db_ = nullptr; - PEImage* img_ = nullptr; + lua_State* L_ = nullptr; + AnalysisDB* db_ = nullptr; + PEImage* img_ = nullptr; std::string output_; std::function nav_cb_; + + // Plugin registry + std::vector plugins_; + std::vector> hotkeys_; // {key_string, cb_ref} + std::vector on_analysis_cbs_; // Lua registry refs + std::string current_plugin_; // set while loading a .lua file + + // Results windows + std::vector result_windows_; }; -} +} // namespace hype diff --git a/src/ui/app.cpp b/src/ui/app.cpp index b7b1728..d227274 100644 --- a/src/ui/app.cpp +++ b/src/ui/app.cpp @@ -149,6 +149,18 @@ int App::run() { out_.log("Hyperion v" HYPERION_VERSION " ready"); out_.log("Drop a PE file or use File > Open (Ctrl+O)"); + // Load plugins from plugins/ dir next to the executable (main thread, pre-loop) + { + auto plugin_dir = std::filesystem::path("plugins"); + lua_.load_plugins(plugin_dir); + auto& loaded = lua_.plugins(); + if (!loaded.empty()) { + int ok = 0, bad = 0; + for (auto& p : loaded) (p.error ? bad : ok)++; + out_.log(fmt::format("Plugins: {} loaded, {} error(s)", ok, bad)); + } + } + while (!renderer_.should_close()) { renderer_.begin_frame(); @@ -196,6 +208,8 @@ int App::run() { sigmaker_.set_data(&db, img_.get()); sigmaker_.set_nav([this](va_t a) { navigate_to(a); sync_panels(a); }); rebuild_nav_band(); + // Fire on_analysis_complete plugin callbacks + lua_.run_analysis_complete_callbacks(); } if (diff_done_.exchange(false)) { @@ -228,6 +242,9 @@ int App::run() { sigmaker_.render(); settings_panel_.render(); + if (show_plugin_manager_) render_plugin_manager(); + render_results_windows(); + if (settings_panel_.theme_changed()) { auto& ct = settings_panel_.custom_theme(); g_custom_theme.bg[0] = ct.bg[0]; g_custom_theme.bg[1] = ct.bg[1]; g_custom_theme.bg[2] = ct.bg[2]; g_custom_theme.bg[3] = ct.bg[3]; @@ -645,6 +662,57 @@ void App::render_menubar() { ImGui::EndMenu(); } + // ---- Plugins menu ---- + if (ImGui::BeginMenu("Plugins")) { + if (ImGui::MenuItem("Plugin Manager...")) + show_plugin_manager_ = true; + ImGui::Separator(); + auto& plgs = lua_.plugins(); + if (plgs.empty()) { + ImGui::TextDisabled("No plugins loaded"); + ImGui::TextDisabled("Place .lua files in plugins/"); + } + for (int pi = 0; pi < static_cast(plgs.size()); ++pi) { + auto& plg = plgs[static_cast(pi)]; + if (plg.error) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.4f, 0.4f, 1.f)); + ImGui::TextDisabled("[!] %s", plg.name.c_str()); + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", plg.error_msg.c_str()); + continue; + } + if (plg.items.size() == 1) { + // Single action — flat menu item labelled by the action + if (ImGui::MenuItem(plg.items[0].label.c_str())) { + lua_.invoke_menu_item(pi, 0); + if (!lua_.last_output().empty()) { + scriptc_.append_output(lua_.last_output()); + ImGui::SetWindowFocus("Script Console"); + } + } + } else if (!plg.items.empty()) { + // Multiple actions — submenu labelled by the plugin name + if (ImGui::BeginMenu(plg.name.c_str())) { + for (int ii = 0; ii < static_cast(plg.items.size()); ++ii) { + if (ImGui::MenuItem(plg.items[static_cast(ii)].label.c_str())) { + lua_.invoke_menu_item(pi, ii); + if (!lua_.last_output().empty()) { + scriptc_.append_output(lua_.last_output()); + ImGui::SetWindowFocus("Script Console"); + } + } + } + ImGui::EndMenu(); + } + } else { + // Plugin registered but added no menu items + ImGui::TextDisabled("%s", plg.name.c_str()); + } + } + ImGui::EndMenu(); + } + float w = ImGui::GetWindowWidth(); if (busy_) { ImGui::SameLine(w - 120); @@ -750,6 +818,9 @@ void App::handle_keys() { if (ImGui::IsKeyPressed(ImGuiKey_Tab) && !io.KeyCtrl) sync_panels(dv_.cursor()); + // Plugin hotkeys (checked last, after built-in keys) + lua_.check_hotkeys(); + if (kb.check("decompile")) { va_t func = find_func_for(dv_.cursor()); if (func) { @@ -1648,4 +1719,170 @@ void App::export_asm() { out_.log(fmt::format("Exported: {}", p.string())); } +void App::render_plugin_manager() { + ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Plugin Manager", &show_plugin_manager_)) { + ImGui::End(); + return; + } + + auto& plgs = lua_.plugins(); + + ImGui::TextDisabled("Plugins directory: plugins/ (%d loaded)", static_cast(plgs.size())); + ImGui::Separator(); + + if (plgs.empty()) { + ImGui::TextDisabled("No plugins found."); + ImGui::TextDisabled("Place .lua files in the plugins/ directory next to the executable."); + } else { + ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp; + if (ImGui::BeginTable("##plugins_tbl", 4, flags, ImVec2(0, -30))) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0.20f); + ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch, 0.40f); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 60.f); + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 120.f); + ImGui::TableHeadersRow(); + + for (int pi = 0; pi < static_cast(plgs.size()); ++pi) { + auto& plg = plgs[static_cast(pi)]; + ImGui::TableNextRow(); + + // Name + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(plg.name.c_str()); + + // Description + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(plg.desc.empty() ? "—" : plg.desc.c_str()); + + // Action count + ImGui::TableSetColumnIndex(2); + ImGui::Text("%d", static_cast(plg.items.size())); + + // Status + ImGui::TableSetColumnIndex(3); + if (plg.error) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.4f, 0.4f, 1.f)); + ImGui::TextUnformatted("Error"); + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", plg.error_msg.c_str()); + } else { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 1.f, 0.5f, 1.f)); + ImGui::TextUnformatted("OK"); + ImGui::PopStyleColor(); + } + } + ImGui::EndTable(); + } + } + + ImGui::End(); +} + +void App::render_results_windows() { + auto& windows = lua_.result_windows(); + for (auto& w : windows) { + if (!w.open) continue; + + ImGui::SetNextWindowSize(ImVec2(820, 480), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSizeConstraints(ImVec2(400, 200), ImVec2(FLT_MAX, FLT_MAX)); + + ImGuiWindowFlags wflags = ImGuiWindowFlags_None; + if (!ImGui::Begin(w.title.c_str(), &w.open, wflags)) { + ImGui::End(); + continue; + } + + // Search / filter bar + ImGui::SetNextItemWidth(-1.f); + ImGui::InputTextWithHint("##rw_filter", "Filter results...", w.filter, sizeof(w.filter)); + ImGui::Separator(); + + std::string filter_lower = w.filter; + for (auto& c : filter_lower) c = static_cast(std::tolower(static_cast(c))); + + // Count visible rows for the footer + int visible = 0; + + ImGuiTableFlags tflags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable | + ImGuiTableFlags_SizingStretchProp; + + // +1 column for the address + int ncols = static_cast(w.headers.size()) + 1; + if (ImGui::BeginTable("##rw_tbl", ncols, tflags, ImVec2(0, -ImGui::GetFrameHeightWithSpacing()))) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthFixed, 110.f); + for (auto& h : w.headers) + ImGui::TableSetupColumn(h.c_str(), ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + + for (int ri = 0; ri < static_cast(w.rows.size()); ++ri) { + auto& row = w.rows[static_cast(ri)]; + + // Apply filter: check address hex + all col values + if (filter_lower.size() > 0) { + char addr_buf[32]; + std::snprintf(addr_buf, sizeof(addr_buf), "%llx", + static_cast(row.addr)); + bool match = (std::string(addr_buf).find(filter_lower) != std::string::npos); + if (!match) { + for (auto& cv : row.cols) { + std::string cvl = cv; + for (auto& c : cvl) c = static_cast(std::tolower(static_cast(c))); + if (cvl.find(filter_lower) != std::string::npos) { match = true; break; } + } + } + if (!match) continue; + } + visible++; + + ImGui::TableNextRow(); + + // Address column — clickable + ImGui::TableSetColumnIndex(0); + char addr_label[48]; + std::snprintf(addr_label, sizeof(addr_label), "0x%llX##row%d", + static_cast(row.addr), ri); + + bool selected = (w.selected == ri); + if (ImGui::Selectable(addr_label, selected, + ImGuiSelectableFlags_SpanAllColumns | + ImGuiSelectableFlags_AllowOverlap, + ImVec2(0, 0))) { + w.selected = ri; + if (img_ && analyzer_) { + navigate_to(row.addr); + sync_panels(row.addr); + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Click to navigate to 0x%llX", + static_cast(row.addr)); + + // Data columns + for (int ci = 0; ci < static_cast(row.cols.size()); ++ci) { + if (ImGui::TableSetColumnIndex(ci + 1)) + ImGui::TextUnformatted(row.cols[static_cast(ci)].c_str()); + } + } + ImGui::EndTable(); + } + + // Footer: count + ImGui::TextDisabled("%d result(s)", visible); + + ImGui::End(); + } + + // Remove windows the user closed + windows.erase( + std::remove_if(windows.begin(), windows.end(), + [](const ResultsWindow& w){ return !w.open; }), + windows.end()); } + +} // namespace hype diff --git a/src/ui/app.h b/src/ui/app.h index 57ec4c6..0b69188 100644 --- a/src/ui/app.h +++ b/src/ui/app.h @@ -79,6 +79,8 @@ class App { void render_nav_band(); void rebuild_nav_band(); void render_bg_image(); + void render_plugin_manager(); + void render_results_windows(); Renderer renderer_; WorkerPool pool_; @@ -130,6 +132,7 @@ class App { bool show_bookmarks_ = false; bool show_sigs_ = false; bool show_apply_type_ = false; + bool show_plugin_manager_ = false; bool layout_built_ = false; char goto_buf_[64] = {}; char rename_buf_[256] = {}; diff --git a/src/ui/widgets/script_console.h b/src/ui/widgets/script_console.h index 751a689..a712a19 100644 --- a/src/ui/widgets/script_console.h +++ b/src/ui/widgets/script_console.h @@ -11,6 +11,9 @@ class ScriptConsole { public: void set_engine(LuaEngine* eng) { engine_ = eng; } void set_nav(std::function cb) { nav_cb_ = std::move(cb); } + void append_output(const std::string& text) { + if (!text.empty()) { output_.push_back(text); scroll_bottom_ = true; } + } void render(); private: