#include #include #include #include "modules/Gui.h" #include "modules/Screen.h" #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" DFHACK_PLUGIN("hotkeys"); namespace DFHack { DBG_DECLARE(hotkeys, log, DebugCategory::LINFO); } using std::map; using std::string; using std::vector; using namespace DFHack; static const string INVOKE_MENU_COMMAND = "overlay trigger hotkeys.menu"; static const string INVOKE_HOTKEYS_COMMAND = "hotkeys"; static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu"; static bool valid = false; // whether the following two vars contain valid data static map current_bindings; static vector sorted_keys; static bool can_invoke(const string &cmdline, df::viewscreen *screen) { vector cmd_parts; split_string(&cmd_parts, cmdline, " "); return Core::getInstance().getPluginManager()->CanInvokeHotkey(cmd_parts[0], screen); } static int cleanupHotkeys(lua_State *) { DEBUG(log).print("cleaning up old stub keybindings for: %s\n", join_strings(", ", Gui::getCurFocus(true)).c_str()); std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); Core::getInstance().ClearKeyBindings(keyspec); }); valid = false; sorted_keys.clear(); current_bindings.clear(); return 0; } static bool should_hide_armok(color_ostream &out, const string &cmdline) { bool should_hide = false; auto L = Lua::Core::State; Lua::StackUnwinder top(L); Lua::CallLuaModuleFunction(out, L, "plugins.hotkeys", "should_hide_armok", 1, 1, [&](lua_State *L){ Lua::Push(L, cmdline); }, [&](lua_State *L){ should_hide = lua_toboolean(L, -1); }); return should_hide; } static void add_binding_if_valid(color_ostream &out, const string &sym, const string &cmdline, df::viewscreen *screen, bool filtermenu) { if (!can_invoke(cmdline, screen)) return; if (filtermenu && (cmdline == INVOKE_MENU_COMMAND || cmdline == INVOKE_HOTKEYS_COMMAND)) { DEBUG(log).print("filtering out hotkey menu keybinding\n"); return; } if (should_hide_armok(out, cmdline)) { DEBUG(log).print("filtering out armok keybinding\n"); return; } current_bindings[sym] = cmdline; sorted_keys.push_back(sym); string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; string binding = "hotkeys invoke " + int_to_string(sorted_keys.size() - 1); DEBUG(log).print("adding keybinding: %s -> %s\n", keyspec.c_str(), binding.c_str()); Core::getInstance().AddKeyBinding(keyspec, binding); } static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, bool filtermenu) { DEBUG(log).print("scanning for active keybindings\n"); if (valid) cleanupHotkeys(NULL); vector valid_keys; for (char c = '0'; c <= '9'; c++) { valid_keys.push_back(string(&c, 1)); } for (char c = 'A'; c <= 'Z'; c++) { valid_keys.push_back(string(&c, 1)); } for (int i = 1; i <= 12; i++) { valid_keys.push_back('F' + int_to_string(i)); } valid_keys.push_back("`"); for (int shifted = 0; shifted < 2; shifted++) { for (int alt = 0; alt < 2; alt++) { for (int ctrl = 0; ctrl < 2; ctrl++) { for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) { string sym; if (ctrl) sym += "Ctrl-"; if (alt) sym += "Alt-"; if (shifted) sym += "Shift-"; sym += *it; auto list = Core::getInstance().ListKeyBindings(sym); for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { string::size_type colon_pos = invoke_cmd->find(":"); // colons at location 0 are for commands like ":lua" if (colon_pos == string::npos || colon_pos == 0) { add_binding_if_valid(out, sym, *invoke_cmd, screen, filtermenu); } else { vector tokens; split_string(&tokens, *invoke_cmd, ":"); string focus = tokens[0].substr(1); if(Gui::matchFocusString(focus)) { auto cmdline = trim(tokens[1]); add_binding_if_valid(out, sym, cmdline, screen, filtermenu); } } } } } } } valid = true; } static int getHotkeys(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); find_active_keybindings(*out, Gui::getCurViewscreen(true), true); Lua::PushVector(L, sorted_keys); Lua::Push(L, current_bindings); return 2; } DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(getHotkeys), DFHACK_LUA_COMMAND(cleanupHotkeys), DFHACK_LUA_END }; static void list(color_ostream &out) { DEBUG(log).print("listing active hotkeys\n"); bool was_valid = valid; if (!valid) find_active_keybindings(out, Gui::getCurViewscreen(true), false); out.print("Valid keybindings for the current focus:\n %s\n", join_strings("\n", Gui::getCurFocus(true)).c_str()); std::for_each(sorted_keys.begin(), sorted_keys.end(), [&](const string &sym) { out.print("%s: %s\n", sym.c_str(), current_bindings[sym].c_str()); }); if (!was_valid) cleanupHotkeys(NULL); } static bool invoke_command(color_ostream &out, const size_t index) { auto screen = Core::getTopViewscreen(); if (sorted_keys.size() <= index || !Gui::matchFocusString(MENU_SCREEN_FOCUS_STRING)) return false; auto cmd = current_bindings[sorted_keys[index]]; DEBUG(log).print("invoking command: '%s'\n", cmd.c_str()); { Screen::Hide hideGuard(screen, Screen::Hide::RESTORE_AT_TOP); Core::getInstance().runCommand(out, cmd); } Screen::dismiss(screen); return true; } static command_result hotkeys_cmd(color_ostream &out, vector & parameters) { if (!parameters.size()) { DEBUG(log).print("invoking command: '%s'\n", INVOKE_MENU_COMMAND.c_str()); return Core::getInstance().runCommand(out, INVOKE_MENU_COMMAND ); } CoreSuspender guard; if (parameters[0] == "list") { list(out); return CR_OK; } // internal command -- intentionally undocumented if (parameters.size() != 2 || parameters[0] != "invoke") return CR_WRONG_USAGE; int index = string_to_int(parameters[1], -1); if (index < 0) return CR_WRONG_USAGE; return invoke_command(out, index) ? CR_OK : CR_WRONG_USAGE; } DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back( PluginCommand( "hotkeys", "Invoke hotkeys from the interactive menu.", hotkeys_cmd, Gui::anywhere_hotkey)); return CR_OK; }