Use helpdb to implement help and ls built-in commands and dfhack.script_help() (#2242)

* use helpdb to implement the help and ls builtins

* use helpdb to implement dfhack.script_help()
develop
Myk 2022-07-08 15:53:51 -07:00 committed by myk002
parent 4ad8e7199a
commit e899510b8b
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
3 changed files with 117 additions and 190 deletions

@ -446,6 +446,7 @@ command_result Core::runCommand(color_ostream &out, const std::string &command)
static const std::set<std::string> built_in_commands = { static const std::set<std::string> built_in_commands = {
"ls" , "ls" ,
"help" , "help" ,
"tags" ,
"type" , "type" ,
"load" , "load" ,
"unload" , "unload" ,
@ -674,25 +675,71 @@ string getBuiltinCommand(std::string cmd)
return builtin; return builtin;
} }
void ls_helper(color_ostream &con, const string &name, const string &desc) void help_helper(color_ostream &con, const string &entry_name) {
{ CoreSuspender suspend;
const size_t help_line_length = 80 - 22 - 5; auto L = Lua::Core::State;
const string padding = string(80 - help_line_length, ' '); Lua::StackUnwinder top(L);
vector<string> lines;
con.print(" %-22s - ", name.c_str()); if (!lua_checkstack(L, 2) ||
word_wrap(&lines, desc, help_line_length); !Lua::PushModulePublic(con, L, "helpdb", "help")) {
con.printerr("Failed to load helpdb Lua code\n");
return;
}
Lua::Push(L, entry_name);
// print first line, then any additional lines preceded by padding if (!Lua::SafeCall(con, L, 1, 0)) {
for (size_t i = 0; i < lines.size(); i++) con.printerr("Failed Lua call to helpdb.help.\n");
con.print("%s%s\n", i ? padding.c_str() : "", lines[i].c_str()); }
} }
void ls_helper(color_ostream &con, const PluginCommand &pcmd) void tags_helper(color_ostream &con) {
{ CoreSuspender suspend;
if (pcmd.isHotkeyCommand()) auto L = Lua::Core::State;
con.color(COLOR_CYAN); Lua::StackUnwinder top(L);
ls_helper(con, pcmd.name, pcmd.description);
con.reset_color(); if (!lua_checkstack(L, 1) ||
!Lua::PushModulePublic(con, L, "helpdb", "tags")) {
con.printerr("Failed to load helpdb Lua code\n");
return;
}
if (!Lua::SafeCall(con, L, 0, 0)) {
con.printerr("Failed Lua call to helpdb.tags.\n");
}
}
void ls_helper(color_ostream &con, const vector<string> &params) {
vector<string> filter;
bool skip_tags = false;
bool show_dev_commands = false;
for (auto str : params) {
if (str == "--notags")
skip_tags = true;
else if (str == "--dev")
show_dev_commands = true;
else
filter.push_back(str);
}
CoreSuspender suspend;
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 4) ||
!Lua::PushModulePublic(con, L, "helpdb", "ls")) {
con.printerr("Failed to load helpdb Lua code\n");
return;
}
Lua::PushVector(L, filter);
Lua::Push(L, skip_tags);
Lua::Push(L, show_dev_commands);
if (!Lua::SafeCall(con, L, 3, 0)) {
con.printerr("Failed Lua call to helpdb.ls.\n");
}
} }
command_result Core::runCommand(color_ostream &con, const std::string &first_, vector<string> &parts) command_result Core::runCommand(color_ostream &con, const std::string &first_, vector<string> &parts)
@ -733,70 +780,33 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
"On Windows, you may have to resize your console window. The appropriate menu is accessible\n" "On Windows, you may have to resize your console window. The appropriate menu is accessible\n"
"by clicking on the program icon in the top bar of the window.\n\n"); "by clicking on the program icon in the top bar of the window.\n\n");
} }
con.print("Basic commands:\n" con.print("Here are some basic commands to get you started:\n"
" help|?|man - This text.\n" " help|?|man - This text.\n"
" help COMMAND - Usage help for the given command.\n" " help <tool> - Usage help for the given plugin, command, or script.\n"
" ls|dir [-a] [PLUGIN] - List available commands. Optionally for single plugin.\n" " tags - List the tags that the DFHack tools are grouped by.\n"
" cls|clear - Clear the console.\n" " ls|dir [<filter>] - List commands, optionally filtered by a tag or substring.\n"
" fpause - Force DF to pause.\n" " Optional parameters:\n"
" die - Force DF to close immediately\n" " --notags: skip printing tags for each command.\n"
" keybinding - Modify bindings of commands to keys\n" " --dev: include commands intended for developers and modders.\n"
"Plugin management (useful for developers):\n" " cls|clear - Clear the console.\n"
" plug [PLUGIN|v] - List plugin state and description.\n" " fpause - Force DF to pause.\n"
" load PLUGIN|-all - Load a plugin by name or load all possible plugins.\n" " die - Force DF to close immediately, without saving.\n"
" unload PLUGIN|-all - Unload a plugin or all loaded plugins.\n" " keybinding - Modify bindings of commands to in-game key shortcuts.\n"
" reload PLUGIN|-all - Reload a plugin or all loaded plugins.\n" "\n"
"See more commands by running 'ls'.\n\n"
); );
con.print("\nDFHack version %s\n", dfhack_version_desc().c_str()); con.print("DFHack version %s\n", dfhack_version_desc().c_str());
}
else if (parts.size() == 1)
{
if (getBuiltinCommand(parts[0]).size())
{
con << parts[0] << ": built-in command; Use `ls`, `help`, or check hack/Readme.html for more information" << std::endl;
return CR_NOT_IMPLEMENTED;
}
Plugin *plug = plug_mgr->getPluginByCommand(parts[0]);
if (plug) {
for (size_t j = 0; j < plug->size();j++)
{
const PluginCommand & pcmd = (plug->operator[](j));
if (pcmd.name != parts[0])
continue;
if (pcmd.isHotkeyCommand())
con.color(COLOR_CYAN);
con.print("%s: %s\n",pcmd.name.c_str(), pcmd.description.c_str());
con.reset_color();
if (!pcmd.usage.empty())
con << "Usage:\n" << pcmd.usage << flush;
return CR_OK;
}
}
string file = findScript(parts[0] + ".lua");
if ( file != "" ) {
string help = getScriptHelp(file, "--");
con.print("%s: %s\n", parts[0].c_str(), help.c_str());
return CR_OK;
}
if (plug_mgr->ruby && plug_mgr->ruby->is_enabled() ) {
file = findScript(parts[0] + ".rb");
if ( file != "" ) {
string help = getScriptHelp(file, "#");
con.print("%s: %s\n", parts[0].c_str(), help.c_str());
return CR_OK;
}
}
con.printerr("Unknown command: %s\n", parts[0].c_str());
return CR_FAILURE;
} }
else else
{ {
con.printerr("not implemented yet\n"); help_helper(con, parts[0]);
return CR_NOT_IMPLEMENTED;
} }
} }
else if (builtin == "tags")
{
tags_helper(con);
}
else if (builtin == "load" || builtin == "unload" || builtin == "reload") else if (builtin == "load" || builtin == "unload" || builtin == "reload")
{ {
bool all = false; bool all = false;
@ -906,83 +916,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
} }
else if (builtin == "ls" || builtin == "dir") else if (builtin == "ls" || builtin == "dir")
{ {
bool all = false; ls_helper(con, parts);
if (parts.size() && parts[0] == "-a")
{
all = true;
vector_erase_at(parts, 0);
}
if(parts.size())
{
string & plugname = parts[0];
const Plugin * plug = (*plug_mgr)[plugname];
if(!plug)
{
con.printerr("There's no plugin called %s!\n", plugname.c_str());
}
else if (plug->getState() != Plugin::PS_LOADED)
{
con.printerr("Plugin %s is not loaded.\n", plugname.c_str());
}
else if (!plug->size())
{
con.printerr("Plugin %s is loaded but does not implement any commands.\n", plugname.c_str());
}
else for (size_t j = 0; j < plug->size();j++)
{
ls_helper(con, plug->operator[](j));
}
}
else
{
con.print(
"builtin:\n"
" help|?|man - This text or help specific to a plugin.\n"
" ls|dir [-a] [PLUGIN] - List available commands. Optionally for single plugin.\n"
" cls|clear - Clear the console.\n"
" fpause - Force DF to pause.\n"
" die - Force DF to close immediately\n"
" kill-lua - Stop an active Lua script\n"
" keybinding - Modify bindings of commands to keys\n"
" script FILENAME - Run the commands specified in a file.\n"
" sc-script - Automatically run specified scripts on state change events\n"
" plug [PLUGIN|v] - List plugin state and detailed description.\n"
" load PLUGIN|-all [...] - Load a plugin by name or load all possible plugins.\n"
" unload PLUGIN|-all [...] - Unload a plugin or all loaded plugins.\n"
" reload PLUGIN|-all [...] - Reload a plugin or all loaded plugins.\n"
" enable/disable PLUGIN [...] - Enable or disable a plugin if supported.\n"
" type COMMAND - Display information about where a command is implemented\n"
"\n"
"plugins:\n"
);
std::set <sortable> out;
for (auto it = plug_mgr->begin(); it != plug_mgr->end(); ++it)
{
const Plugin * plug = it->second;
if(!plug->size())
continue;
for (size_t j = 0; j < plug->size();j++)
{
const PluginCommand & pcmd = (plug->operator[](j));
out.insert(sortable(pcmd.isHotkeyCommand(),pcmd.name,pcmd.description));
}
}
for(auto iter = out.begin();iter != out.end();iter++)
{
if ((*iter).recolor)
con.color(COLOR_CYAN);
ls_helper(con, iter->name, iter->description);
con.reset_color();
}
std::map<string, string> scripts;
listAllScripts(scripts, all);
if (!scripts.empty())
{
con.print("\nscripts:\n");
for (auto iter = scripts.begin(); iter != scripts.end(); ++iter)
ls_helper(con, iter->first, iter->second);
}
}
} }
else if (builtin == "plug") else if (builtin == "plug")
{ {

@ -1,4 +1,4 @@
-- Common startup file for all dfhack plugins with lua support -- Common startup file for all dfhack scripts and plugins with lua support
-- The global dfhack table is already created by C++ init code. -- The global dfhack table is already created by C++ init code.
-- Setup the global environment. -- Setup the global environment.
@ -652,8 +652,9 @@ function Script:needs_update()
return (not self.env) or self.mtime ~= dfhack.filesystem.mtime(self.path) return (not self.env) or self.mtime ~= dfhack.filesystem.mtime(self.path)
end end
function Script:get_flags() function Script:get_flags()
if self.flags_mtime ~= dfhack.filesystem.mtime(self.path) then local mtime = dfhack.filesystem.mtime(self.path)
self.flags_mtime = dfhack.filesystem.mtime(self.path) if self.flags_mtime ~= mtime then
self.flags_mtime = mtime
self._flags = {} self._flags = {}
local f = io.open(self.path) local f = io.open(self.path)
local contents = f:read('*all') local contents = f:read('*all')
@ -814,36 +815,7 @@ end
function dfhack.script_help(script_name, extension) function dfhack.script_help(script_name, extension)
script_name = script_name or dfhack.current_script_name() script_name = script_name or dfhack.current_script_name()
extension = extension or 'lua' return require('helpdb').get_entry_long_help(script_name)
local full_name = script_name .. '.' .. extension
local path = dfhack.internal.findScript(script_name .. '.' .. extension)
or error("Could not find script: " .. full_name)
local begin_seq, end_seq
if extension == 'rb' then
begin_seq = '=begin'
end_seq = '=end'
else
begin_seq = '[====['
end_seq = ']====]'
end
local f = io.open(path) or error("Could not open " .. path)
local in_help = false
local help = ''
for line in f:lines() do
if line:endswith(begin_seq) then
in_help = true
elseif in_help then
if line:endswith(end_seq) then
break
end
if line ~= script_name and line ~= ('='):rep(#script_name) then
help = help .. line .. '\n'
end
end
end
f:close()
help = help:gsub('^\n+', ''):gsub('\n+$', '')
return help
end end
local function _run_command(args, use_console) local function _run_command(args, use_console)

@ -326,11 +326,13 @@ local function scan_scripts(old_db, db)
f.path:startswith('internal/') then f.path:startswith('internal/') then
goto continue goto continue
end end
local dot_index = f.path:find('%.[^.]*$')
local script_source = script_path .. '/' .. f.path local script_source = script_path .. '/' .. f.path
update_db(old_db, db, update_db(old_db, db,
has_rendered_help(f.path) and has_rendered_help(f.path) and
HELP_SOURCES.RENDERED or HELP_SOURCES.SCRIPT, HELP_SOURCES.RENDERED or HELP_SOURCES.SCRIPT,
f.path:sub(1, #f.path - 4), {script_source=script_source}) f.path:sub(1, dot_index - 1),
{script_source=script_source})
::continue:: ::continue::
end end
::skip_path:: ::skip_path::
@ -384,7 +386,7 @@ end
local function get_db_property(entry_name, property) local function get_db_property(entry_name, property)
ensure_db() ensure_db()
if not db[entry_name] then if not db[entry_name] then
error(('entry not found: "%s"'):format(entry_name)) error(('helpdb entry not found: "%s"'):format(entry_name))
end end
return db[entry_name][property] return db[entry_name][property]
end end
@ -415,8 +417,17 @@ end
-- returns whether the given string matches a tag name -- returns whether the given string matches a tag name
function is_tag(str) function is_tag(str)
if not str or #str == 0 then
return false
end
ensure_db() ensure_db()
return not not tag_index[str] if type(str) == "string" then str = {str} end
for _,s in ipairs(str) do
if not tag_index[s] then
return false
end
end
return true
end end
-- returns the defined tags in alphabetical order -- returns the defined tags in alphabetical order
@ -510,7 +521,7 @@ end
-- converts strings into single-element lists containing that string -- converts strings into single-element lists containing that string
local function normalize_string_list(l) local function normalize_string_list(l)
if not l then return nil end if not l or #l == 0 then return nil end
if type(l) == 'string' then if type(l) == 'string' then
return {l} return {l}
end end
@ -561,9 +572,19 @@ function search_entries(include, exclude)
end end
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
-- list API (outputs to console) -- print API (outputs to console)
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
-- implements the 'help' builtin command
function help(entry)
ensure_db()
if not db[entry] then
dfhack.printerr(('No help entry found for "%s"'):format(entry))
return
end
print(get_entry_long_help(entry))
end
local function get_max_width(list, min_width) local function get_max_width(list, min_width)
local width = min_width or 0 local width = min_width or 0
for _,item in ipairs(list) do for _,item in ipairs(list) do
@ -572,8 +593,8 @@ local function get_max_width(list, min_width)
return width return width
end end
-- prints the defined tags and their descriptions to the console -- implements the 'tags' builtin command
function list_tags() function tags()
local tags = get_tags() local tags = get_tags()
local width = get_max_width(tags, 10) local width = get_max_width(tags, 10)
for _,tag in ipairs(tags) do for _,tag in ipairs(tags) do