From 4ad8e7199a691f900cac1824d0f746924eb748c4 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 8 Jul 2022 14:43:41 -0700 Subject: [PATCH] Support builtin commands in helpdb (#2241) * support builtin commands in helpdb, implement list API, document api --- docs/Builtin.rst | 269 +++++++++++++++++++++++++++ docs/Core.rst | 264 +-------------------------- index.rst | 1 + library/lua/helpdb.lua | 405 ++++++++++++++++++++++++++++------------- 4 files changed, 559 insertions(+), 380 deletions(-) create mode 100644 docs/Builtin.rst diff --git a/docs/Builtin.rst b/docs/Builtin.rst new file mode 100644 index 000000000..2d938e30f --- /dev/null +++ b/docs/Builtin.rst @@ -0,0 +1,269 @@ +.. _built-in-commands: + +Built-in Commands +================= +The following commands are provided by the 'core' components of DFHack, rather +than plugins or scripts. + +.. contents:: + :local: + +.. _alias: + +alias +----- +The ``alias`` command allows configuring aliases to other DFHack commands. +Aliases are resolved immediately after built-in commands, which means that an +alias cannot override a built-in command, but can override a command implemented +by a plugin or script. + +Usage: + +:``alias list``: lists all configured aliases +:``alias add [arguments...]``: adds an alias +:``alias replace [arguments...]``: replaces an existing + alias with a new command, or adds the alias if it does not already exist +:``alias delete ``: removes the specified alias + +Aliases can be given additional arguments when created and invoked, which will +be passed to the underlying command in order. An example with +`devel/print-args`:: + + [DFHack]# alias add pargs devel/print-args example + [DFHack]# pargs text + example + text + + +.. _cls: + +cls +--- +Clear the terminal. Does not delete command history. + + +.. _die: + +die +--- +Instantly kills DF without saving. + + +.. _disable: +.. _enable: + +enable +------ +Many plugins and scripts can be in a distinct enabled or disabled state. Some of +them activate and deactivate automatically depending on the contents of the +world raws. Others store their state in world data. However a number of them +have to be enabled globally, and the init file is the right place to do it. + +Most such plugins or scripts support the built-in ``enable`` and ``disable`` +commands. Calling them at any time without arguments prints a list of enabled +and disabled plugins, and shows whether that can be changed through the same +commands. Passing plugin names to these commands will enable or disable the +specified plugins. For example, to enable the `manipulator` plugin:: + + enable manipulator + +It is also possible to enable or disable multiple plugins at once:: + + enable manipulator search + + +.. _fpause: + +fpause +------ +Forces DF to pause. This is useful when your FPS drops below 1 and you lose +control of the game. + + +.. _help: + +help +---- +Most commands support using the ``help `` built-in command to retrieve +further help without having to look online. ``? `` and ``man `` are +aliases. + +Some commands (including many scripts) instead take ``help`` or ``?`` as an +option on their command line - ie `` help``. + + +.. _hide: + +hide +---- +Hides the DFHack terminal window. Only available on Windows. + + +.. _keybinding: + +keybinding +---------- +To set keybindings, use the built-in ``keybinding`` command. Like any other +command it can be used at any time from the console, but bindings are not +remembered between runs of the game unless re-created in `dfhack.init`. + +Currently, any combinations of Ctrl/Alt/Shift with A-Z, 0-9, or F1-F12 are +supported. + +Possible ways to call the command: + +``keybinding list `` + List bindings active for the key combination. +``keybinding clear ...`` + Remove bindings for the specified keys. +``keybinding add "cmdline" "cmdline"...`` + Add bindings for the specified key. +``keybinding set "cmdline" "cmdline"...`` + Clear, and then add bindings for the specified key. + +The ```` parameter above has the following *case-sensitive* syntax:: + + [Ctrl-][Alt-][Shift-]KEY[@context[|context...]] + +where the *KEY* part can be any recognized key and [] denote optional parts. + +When multiple commands are bound to the same key combination, DFHack selects +the first applicable one. Later ``add`` commands, and earlier entries within one +``add`` command have priority. Commands that are not specifically intended for +use as a hotkey are always considered applicable. + +The ``context`` part in the key specifier above can be used to explicitly +restrict the UI state where the binding would be applicable. If called without +parameters, the ``keybinding`` command among other things prints the current +context string. + +Only bindings with a ``context`` tag that either matches the current context +fully, or is a prefix ending at a ``/`` boundary would be considered for +execution, i.e. when in context ``foo/bar/baz``, keybindings restricted to any +of ``@foo/bar/baz``, ``@foo/bar``, ``@foo`` or none will be active. + +Multiple contexts can be specified by separating them with a pipe (``|``) - for +example, ``@foo|bar|baz/foo`` would match anything under ``@foo``, ``@bar``, or +``@baz/foo``. + +Interactive commands like `liquids` cannot be used as hotkeys. + + +.. _kill-lua: + +kill-lua +-------- +Stops any currently-running Lua scripts. By default, scripts can only be +interrupted every 256 instructions. Use ``kill-lua force`` to interrupt the next +instruction. + + +.. _load: +.. _unload: +.. _reload: + +load +---- +``load``, ``unload``, and ``reload`` control whether a plugin is loaded into +memory - note that plugins are loaded but disabled unless you explicitly enable +them. Usage:: + + load|unload|reload PLUGIN|(-a|--all) + +Allows dealing with plugins individually by name, or all at once. + +Note that plugins do not maintain their enabled state if they are reloaded, so +you may need to use `enable` to re-enable a plugin after reloading it. + + +.. _ls: +.. _dir: + +ls +-- +``ls`` (or ``dir``) does not list files like the Unix command, but rather +available commands. In order to group related commands, each command is +associated with a list of tags. You can filter the listed commands by a +tag or a substring of the command name. Usage: + +:``ls``: Lists all available commands and the tags associated with them + (if any). +:``ls TAG``: Shows only commands that have the given tag. Use the `tags` command + to see the list of available tags. +:``ls STRING``: Shows commands that include the given string. E.g. ``ls auto`` + will show all the commands with "auto" in their names. If the string is also + the name of a tag, then it will be interpreted as a tag name. + +You can also pass some optional parameters to change how ``ls`` behaves: + +:``--notags``: Don't print out the tags associated with each command. +:``--dev``: Include commands intended for developers and modders. + + +.. _plug: + +plug +---- +Lists available plugins and whether they are enabled. + +``plug`` + Lists available plugins (*not* commands implemented by plugins) +``plug [PLUGIN] [PLUGIN] ...`` + List state and detailed description of the given plugins, + including commands implemented by the plugin. + + +.. _sc-script: + +sc-script +--------- +Allows additional scripts to be run when certain events occur (similar to +onLoad\*.init scripts) + + +.. _script: + +script +------ +Reads a text file, and runs each line as a DFHack command as if it had been +typed in by the user - treating the input like `an init file `. + +Some other tools, such as `autobutcher` and `workflow`, export their settings as +the commands to create them - which can later be reloaded with ``script``. + + +.. _show: + +show +---- +Shows the terminal window after it has been `hidden `. Only available on +Windows. You'll need to use it from a `keybinding` set beforehand, or the +in-game `command-prompt`. + + +.. _tags: + +tags +---- + +List the strings that the DFHack tools can be tagged with. You can find groups +of related tools by passing the tag name to `ls`. + +.. _type: + +type +---- +``type command`` shows where ``command`` is implemented. + +Other Commands +-------------- +The following commands are *not* built-in, but offer similarly useful functions. + +* `command-prompt` +* `hotkeys` +* `lua` +* `multicmd` +* `nopause` +* `quicksave` +* `rb` +* `repeat` diff --git a/docs/Core.rst b/docs/Core.rst index fe881a54d..ffa620f55 100644 --- a/docs/Core.rst +++ b/docs/Core.rst @@ -11,22 +11,23 @@ DFHack Core Command Implementation ====================== -DFHack commands can be implemented in three ways, all of which -are used in the same way: +DFHack commands can be implemented in any of three ways: :builtin: commands are implemented by the core of DFHack. They manage other DFHack tools, interpret commands, and control basic - aspects of DF (force pause or quit). + aspects of DF (force pause or quit). They are documented + `here `. :plugins: are stored in ``hack/plugins/`` and must be compiled with the same version of DFHack. They are less flexible than scripts, but used for complex or ongoing tasks because they run faster. + Plugins included with DFHack are documented `here `. :scripts: are Ruby or Lua scripts stored in ``hack/scripts/``. Because they don't need to be compiled, scripts are more flexible about versions, and easier to distribute. - Most third-party DFHack addons are scripts. - + Most third-party DFHack addons are scripts. All scripts included + with DFHack are documented `here `. Using DFHack Commands ===================== @@ -113,259 +114,6 @@ second (Windows) example uses `kill-lua` to stop a Lua script. you have multiple copies of DF running simultaneously. To assign a different port, see `remote-server-config`. - -Built-in Commands -================= -The following commands are provided by the 'core' components -of DFHack, rather than plugins or scripts. - -.. contents:: - :local: - - -.. _alias: - -alias ------ -The ``alias`` command allows configuring aliases to other DFHack commands. -Aliases are resolved immediately after built-in commands, which means that an -alias cannot override a built-in command, but can override a command implemented -by a plugin or script. - -Usage: - -:``alias list``: lists all configured aliases -:``alias add [arguments...]``: adds an alias -:``alias replace [arguments...]``: replaces an existing - alias with a new command, or adds the alias if it does not already exist -:``alias delete ``: removes the specified alias - -Aliases can be given additional arguments when created and invoked, which will -be passed to the underlying command in order. An example with `devel/print-args`:: - - [DFHack]# alias add pargs devel/print-args example - [DFHack]# pargs text - example - text - - -.. _cls: - -cls ---- -Clear the terminal. Does not delete command history. - - -.. _die: - -die ---- -Instantly kills DF without saving. - - -.. _disable: - -.. _enable: - -enable ------- -Many plugins can be in a distinct enabled or disabled state. Some of -them activate and deactivate automatically depending on the contents -of the world raws. Others store their state in world data. However a -number of them have to be enabled globally, and the init file is the -right place to do it. - -Most such plugins or scripts support the built-in ``enable`` and ``disable`` -commands. Calling them at any time without arguments prints a list -of enabled and disabled plugins, and shows whether that can be changed -through the same commands. Passing plugin names to these commands will enable -or disable the specified plugins. For example, to enable the `manipulator` -plugin:: - - enable manipulator - -It is also possible to enable or disable multiple plugins at once:: - - enable manipulator search - - -.. _fpause: - -fpause ------- -Forces DF to pause. This is useful when your FPS drops below 1 and you lose -control of the game. - - -.. _help: - -help ----- -Most commands support using the ``help `` built-in command -to retrieve further help without having to look at this document. -``? `` and ``man `` are aliases. - -Some commands (including many scripts) instead take ``help`` or ``?`` -as an option on their command line - ie `` help``. - - -.. _hide: - -hide ----- -Hides the DFHack terminal window. Only available on Windows. - - -.. _keybinding: - -keybinding ----------- -To set keybindings, use the built-in ``keybinding`` command. Like any other -command it can be used at any time from the console, but bindings are not -remembered between runs of the game unless re-created in `dfhack.init`. - -Currently, any combinations of Ctrl/Alt/Shift with A-Z, 0-9, or F1-F12 are supported. - -Possible ways to call the command: - -``keybinding list `` - List bindings active for the key combination. -``keybinding clear ...`` - Remove bindings for the specified keys. -``keybinding add "cmdline" "cmdline"...`` - Add bindings for the specified key. -``keybinding set "cmdline" "cmdline"...`` - Clear, and then add bindings for the specified key. - -The ```` parameter above has the following *case-sensitive* syntax:: - - [Ctrl-][Alt-][Shift-]KEY[@context[|context...]] - -where the *KEY* part can be any recognized key and [] denote optional parts. - -When multiple commands are bound to the same key combination, DFHack selects -the first applicable one. Later ``add`` commands, and earlier entries within one -``add`` command have priority. Commands that are not specifically intended for use -as a hotkey are always considered applicable. - -The ``context`` part in the key specifier above can be used to explicitly restrict -the UI state where the binding would be applicable. If called without parameters, -the ``keybinding`` command among other things prints the current context string. - -Only bindings with a ``context`` tag that either matches the current context fully, -or is a prefix ending at a ``/`` boundary would be considered for execution, i.e. -when in context ``foo/bar/baz``, keybindings restricted to any of ``@foo/bar/baz``, -``@foo/bar``, ``@foo`` or none will be active. - -Multiple contexts can be specified by separating them with a -pipe (``|``) - for example, ``@foo|bar|baz/foo`` would match -anything under ``@foo``, ``@bar``, or ``@baz/foo``. - -Interactive commands like `liquids` cannot be used as hotkeys. - - -.. _kill-lua: - -kill-lua --------- -Stops any currently-running Lua scripts. By default, scripts can -only be interrupted every 256 instructions. Use ``kill-lua force`` -to interrupt the next instruction. - - -.. _load: -.. _unload: -.. _reload: - -load ----- -``load``, ``unload``, and ``reload`` control whether a plugin is loaded -into memory - note that plugins are loaded but disabled unless you do -something. Usage:: - - load|unload|reload PLUGIN|(-a|--all) - -Allows dealing with plugins individually by name, or all at once. - -Note that plugins do not maintain their enabled state if they are reloaded, so -you may need to use `enable` to re-enable a plugin after reloading it. - - -.. _ls: - -ls --- -``ls`` does not list files like the Unix command, but rather -available commands - first built in commands, then plugins, -and scripts at the end. Usage: - -:ls -a: Also list scripts in subdirectories of ``hack/scripts/``, - which are generally not intended for direct use. -:ls : List subcommands for the given plugin. - - -.. _plug: - -plug ----- -Lists available plugins, including their state and detailed description. - -``plug`` - Lists available plugins (*not* commands implemented by plugins) -``plug [PLUGIN] [PLUGIN] ...`` - List state and detailed description of the given plugins, - including commands implemented by the plugin. - - -.. _sc-script: - -sc-script ---------- -Allows additional scripts to be run when certain events occur -(similar to onLoad*.init scripts) - - -.. _script: - -script ------- -Reads a text file, and runs each line as a DFHack command -as if it had been typed in by the user - treating the -input like `an init file `. - -Some other tools, such as `autobutcher` and `workflow`, export -their settings as the commands to create them - which are later -loaded with ``script`` - - -.. _show: - -show ----- -Shows the terminal window after it has been `hidden `. -Only available on Windows. You'll need to use it from a -`keybinding` set beforehand, or the in-game `command-prompt`. - -.. _type: - -type ----- -``type command`` shows where ``command`` is implemented. - -Other Commands --------------- -The following commands are *not* built-in, but offer similarly useful functions. - -* `command-prompt` -* `hotkeys` -* `lua` -* `multicmd` -* `nopause` -* `quicksave` -* `rb` -* `repeat` - - .. _dfhack-config: Configuration Files diff --git a/index.rst b/index.rst index 3e761bbc6..3b46699a7 100644 --- a/index.rst +++ b/index.rst @@ -30,6 +30,7 @@ User Manual /docs/Installing /docs/Support /docs/Core + /docs/Builtin /docs/Plugins /docs/Scripts /docs/Tags diff --git a/library/lua/helpdb.lua b/library/lua/helpdb.lua index 8a27abad8..2999ee67e 100644 --- a/library/lua/helpdb.lua +++ b/library/lua/helpdb.lua @@ -1,80 +1,122 @@ --- The help text database. +-- The help text database and query interface. -- --- Command help is read from the the following sources: --- 1. rendered text in hack/docs/docs/ --- 2. (for scripts) the script sources if no pre-rendered text exists or if the --- script file has a modification time that is more recent than the --- pre-rendered text --- 3. (for plugins) the string passed to the PluginCommand initializer if no --- pre-rendered text exists +-- Help text is read from the rendered text in hack/docs/docs/. If no rendered +-- text exists, it is read from the script sources (for scripts) or the string +-- passed to the PluginCommand initializer (for plugins). -- --- For plugins that don't register any commands, the plugin name serves as the --- command so documentation on what happens when you enable the plugin can be --- found. +-- For plugins that don't register a command with the same name as the plugin, +-- the plugin name is registered as a separate entry so documentation on what +-- happens when you enable the plugin can be found. +-- +-- The database is lazy-loaded when an API method is called. It rechecks its +-- help sources for updates if an API method has not been called in the last +-- 60 seconds. local _ENV = mkmodule('helpdb') +-- paths local RENDERED_PATH = 'hack/docs/docs/tools/' +local BUILTIN_HELP = 'hack/docs/docs/Builtin.txt' local TAG_DEFINITIONS = 'hack/docs/docs/Tags.txt' +-- used when reading help text embedded in script sources local SCRIPT_DOC_BEGIN = '[====[' local SCRIPT_DOC_END = ']====]' +local SCRIPT_DOC_BEGIN_RUBY = '=begin' +local SCRIPT_DOC_END_RUBY = '=end' + +local ENTRY_TYPES = { + BUILTIN='builtin', + PLUGIN='plugin', + COMMAND='command' +} -local SOURCES = { +local HELP_SOURCES = { STUB='stub', RENDERED='rendered', PLUGIN='plugin', SCRIPT='script', } --- command name -> {short_help, long_help, tags, source, source_timestamp} --- also includes a script_source_path element if the source is a script --- and a unrunnable boolean if the source is a plugin that does not provide any --- commands to invoke directly. +-- entry name -> { +-- entry_types (set of ENTRY_TYPES), +-- short_help (string), +-- long_help (string), +-- tags (set), +-- help_source (element of HELP_SOURCES), +-- source_timestamp (mtime, 0 for non-files), +-- source_path (string, nil for non-files) +-- } +-- +-- entry_types is a set because plugin commands can also be the plugin names. db = db or {} --- tag name -> list of command names +-- tag name -> list of entry names +-- Tags defined in the TAG_DEFINITIONS file that have no associated db entries +-- will have an empty list. tag_index = tag_index or {} -local function get_rendered_path(command) - return RENDERED_PATH .. command .. '.txt' +local function get_rendered_path(entry_name) + return RENDERED_PATH .. entry_name .. '.txt' end -local function has_rendered_help(command) - return dfhack.filesystem.mtime(get_rendered_path(command)) ~= -1 +local function has_rendered_help(entry_name) + return dfhack.filesystem.mtime(get_rendered_path(entry_name)) ~= -1 end local DEFAULT_HELP_TEMPLATE = [[ %s %s -Tags: None - No help available. ]] - -local function make_default_entry(command, source) +local function make_default_entry(entry_name, entry_types, source, + source_timestamp, source_path) local default_long_help = DEFAULT_HELP_TEMPLATE:format( - command, ('*'):rep(#command)) - return {short_help='No help available.', long_help=default_long_help, - tags={}, source=source, source_timestamp=0} + entry_name, ('*'):rep(#entry_name)) + return { + entry_types=entry_types, + short_help='No help available.', + long_help=default_long_help, + tags={}, + help_source=source, + source_timestamp=source_timestamp or 0, + source_path=source_path} end --- updates the short_text, the long_text, and the tags in the given entry +-- updates the short_text, the long_text, and the tags in the given entry based +-- on the text returned from the iterator. +-- if defined, opts can have the following fields: +-- begin_marker (string that marks the beginning of the help text; all text +-- before this marker is ignored) +-- end_marker (string that marks the end of the help text; text will stop +-- being parsed after this marker is seen) +-- no_header (don't try to find the entity name at the top of the help text) +-- first_line_is_short_help (read the short help text from the first commented +-- line of the script instead of using the first +-- sentence of the long help text) local function update_entry(entry, iterator, opts) opts = opts or {} local lines = {} + local first_line_is_short_help = opts.first_line_is_short_help local begin_marker_found,header_found = not opts.begin_marker,opts.no_header local tags_found, short_help_found, in_short_help = false, false, false for line in iterator do - if not short_help_found and opts.first_line_is_short_help then - local _,_,text = line:trim():find('^%-%-%s*(.*)') - if not text:endswith('.') then - text = text .. '.' + if not short_help_found and first_line_is_short_help then + line = line:trim() + local _,_,text = line:find('^%-%-%s*(.*)') or line:find('^#%s*(.*)') + if not text then + -- if no first-line short help found, fall back to getting the + -- first sentence of the help text. + first_line_is_short_help = false + else + if not text:endswith('.') then + text = text .. '.' + end + entry.short_help = text + short_help_found = true + goto continue end - entry.short_help = text - short_help_found = true - goto continue end if not begin_marker_found then local _, endpos = line:find(opts.begin_marker, 1, true) @@ -119,80 +161,131 @@ local function update_entry(entry, iterator, opts) entry.long_help = table.concat(lines, '\n') end -local function make_rendered_entry(old_entry, command) - local rendered_path = get_rendered_path(command) +-- create db entry based on parsing sphinx-rendered help text +local function make_rendered_entry(old_entry, entry_name, entry_types) + local rendered_path = get_rendered_path(entry_name) local source_timestamp = dfhack.filesystem.mtime(rendered_path) - if old_entry and old_entry.source == SOURCES.RENDERED and + if old_entry and old_entry.source == HELP_SOURCES.RENDERED and old_entry.source_timestamp >= source_timestamp then -- we already have the latest info return old_entry end - local entry = make_default_entry(command, SOURCES.RENDERED) + local entry = make_default_entry(entry_name, entry_types, + HELP_SOURCES.RENDERED, source_timestamp, rendered_path) update_entry(entry, io.lines(rendered_path)) - entry.source_timestamp = source_timestamp return entry end -local function make_plugin_entry(old_entry, command) - if old_entry and old_entry.source == SOURCES.PLUGIN then +-- create db entry based on the help text in the plugin source (used by +-- out-of-tree plugins) +local function make_plugin_entry(old_entry, entry_name, entry_types) + if old_entry and old_entry.source == HELP_SOURCES.PLUGIN then -- we can't tell when a plugin is reloaded, so we can either choose to -- always refresh or never refresh. let's go with never for now for -- performance. return old_entry end - local entry = make_default_entry(command, SOURCES.PLUGIN) - local long_help = dfhack.internal.getCommandHelp(command) + local entry = make_default_entry(entry_name, entry_types, + HELP_SOURCES.PLUGIN) + local long_help = dfhack.internal.getCommandHelp(entry_name) if long_help and #long_help:trim() > 0 then update_entry(entry, long_help:trim():gmatch('[^\n]*'), {no_header=true}) end return entry end -local function make_script_entry(old_entry, command, script_source_path) +-- create db entry based on the help text in the script source (used by +-- out-of-tree scripts) +local function make_script_entry(old_entry, entry_name, script_source_path) local source_timestamp = dfhack.filesystem.mtime(script_source_path) - if old_entry and old_entry.source == SOURCES.SCRIPT and + if old_entry and old_entry.source == HELP_SOURCES.SCRIPT and old_entry.script_source_path == script_source_path and old_entry.source_timestamp >= source_timestamp then -- we already have the latest info return old_entry end - local entry = make_default_entry(command, SOURCES.SCRIPT) + local entry = make_default_entry(entry_name, {[ENTRY_TYPES.COMMAND]=true}, + HELP_SOURCES.SCRIPT, source_timestamp, script_source_path) + local is_rb = script_source_path:endswith('.rb') update_entry(entry, io.lines(script_source_path), - {begin_marker=SCRIPT_DOC_BEGIN, end_marker=SCRIPT_DOC_END, - first_line_is_short_help=true}) - entry.source_timestamp = source_timestamp + {begin_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_BEGIN), + end_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_END), + first_line_is_short_help=true}) return entry end -local function update_db(old_db, db, source, command, flags) - if db[command] then +-- updates the db (and associated tag index) with a new entry if the entry_name +-- doesn't already exist in the db. +local function update_db(old_db, db, source, entry_name, kwargs) + if db[entry_name] then -- already in db (e.g. from a higher-priority script dir); skip return end - local entry, old_entry = nil, old_db[command] - if source == SOURCES.RENDERED then - entry = make_rendered_entry(old_entry, command) - elseif source == SOURCES.PLUGIN then - entry = make_plugin_entry(old_entry, command) - elseif source == SOURCES.SCRIPT then - entry = make_script_entry(old_entry, command, flags.script_source) - elseif source == SOURCES.STUB then - entry = make_default_entry(command, SOURCES.STUB) + local entry, old_entry = nil, old_db[entry_name] + if source == HELP_SOURCES.RENDERED then + entry = make_rendered_entry(old_entry, entry_name, kwargs.entry_types) + elseif source == HELP_SOURCES.PLUGIN then + entry = make_plugin_entry(old_entry, entry_name, kwargs.entry_types) + elseif source == HELP_SOURCES.SCRIPT then + entry = make_script_entry(old_entry, entry_name, kwargs.script_source) + elseif source == HELP_SOURCES.STUB then + entry = make_default_entry(entry_name, kwargs.entry_types, + HELP_SOURCES.STUB) else error('unhandled help source: ' .. source) end - - entry.unrunnable = (flags or {}).unrunnable - - db[command] = entry + db[entry_name] = entry for _,tag in ipairs(entry.tags) do - -- unknown tags are ignored + -- ignore unknown tags if tag_index[tag] then - table.insert(tag_index[tag], command) + table.insert(tag_index[tag], entry_name) end end end +local BUILTINS = { + alias='Configure helper aliases for other DFHack commands.', + cls='Clear the console screen.', + clear='Clear the console screen.', + die='Force DF to close immediately, without saving.', + enable='Enable a plugin or persistent script.', + disable='Disable a plugin or persistent script.', + fpause='Force DF to pause.', + help='Usage help for the given plugin, command, or script.', + hide='Hide the terminal window (Windows only).', + keybinding='Modify bindings of commands to in-game key shortcuts.', + ['kill-lua']='Stop a misbehaving Lua script.', + ['load']='Load and register a plugin library.', + unload='Unregister and unload a plugin.', + reload='Unload and reload a plugin library.', + ls='List commands, optionally filtered by a tag or substring.', + dir='List commands, optionally filtered by a tag or substring.', + plug='List plugins and whether they are enabled.', + ['sc-script']='Automatically run specified scripts on state change events.', + script='Run commands specified in a file.', + show='Show a hidden terminal window (Windows only).', + tags='List the tags that the DFHack tools are grouped by.', + ['type']='Discover how a command is implemented.', +} + +-- add the builtin commands to the db +local function scan_builtins(old_db, db) + local entry = make_default_entry('builtin', + {[ENTRY_TYPES.BUILTIN]=true, [ENTRY_TYPES.COMMAND]=true}, + HELP_SOURCES.RENDERED, 0, BUILTIN_HELP) + -- read in builtin help + local f = io.open(BUILTIN_HELP) + if f then + entry.long_help = f:read('*all') + end + for b,short_help in pairs(BUILTINS) do + local builtin_entry = copyall(entry) + builtin_entry.short_help = short_help + db[b] = builtin_entry + end +end + +-- scan for plugins and plugin-provided commands and add their help to the db local function scan_plugins(old_db, db) local plugin_names = dfhack.internal.listPlugins() for _,plugin in ipairs(plugin_names) do @@ -202,36 +295,41 @@ local function scan_plugins(old_db, db) -- documentation to update_db(old_db, db, has_rendered_help(plugin) and - SOURCES.RENDERED or SOURCES.STUB, - plugin, {unrunnable=true}) + HELP_SOURCES.RENDERED or HELP_SOURCES.STUB, + plugin, {entry_types={[ENTRY_TYPES.PLUGIN]=true}}) goto continue end for _,command in ipairs(commands) do + local entry_types = {[ENTRY_TYPES.COMMAND]=true} + if command == plugin then + entry_types[ENTRY_TYPES.PLUGIN]=true + end update_db(old_db, db, has_rendered_help(command) and - SOURCES.RENDERED or SOURCES.PLUGIN, - command) + HELP_SOURCES.RENDERED or HELP_SOURCES.PLUGIN, + command, {entry_types=entry_types}) end ::continue:: end end +-- scan for scripts and add their help to the db local function scan_scripts(old_db, db) for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do local files = dfhack.filesystem.listdir_recursive( script_path, nil, false) if not files then goto skip_path end for _,f in ipairs(files) do - if f.isdir or not f.path:endswith('.lua') or + if f.isdir or + (not f.path:endswith('.lua') and not f.path:endswith('.rb')) or f.path:startswith('test/') or f.path:startswith('internal/') then goto continue end local script_source = script_path .. '/' .. f.path - local script_is_newer = dfhack.filesystem.mtime(script_source) > - dfhack.filesystem.mtime(get_rendered_path(f.path)) update_db(old_db, db, - script_is_newer and SOURCES.SCRIPT or SOURCES.RENDERED, + has_rendered_help(f.path) and + HELP_SOURCES.RENDERED or HELP_SOURCES.SCRIPT, f.path:sub(1, #f.path - 4), {script_source=script_source}) ::continue:: end @@ -239,6 +337,8 @@ local function scan_scripts(old_db, db) end end +-- read tags and descriptions from the TAG_DEFINITIONS file and add them all +-- to tag_index, initizlizing each entry with an empty list. local function initialize_tags() local tag, desc, in_desc = nil, nil, false for line in io.lines(TAG_DEFINITIONS) do @@ -261,7 +361,7 @@ local function initialize_tags() end -- ensures the db is up to date by scanning all help sources. does not do --- anything if it has already been run within the last 10 seconds. +-- anything if it has already been run within the last 60 seconds. last_refresh_ms = last_refresh_ms or 0 local function ensure_db() local now_ms = dfhack.getTickCount() @@ -272,16 +372,21 @@ local function ensure_db() db, tag_index = {}, {} initialize_tags() + scan_builtins(old_db, db) scan_plugins(old_db, db) scan_scripts(old_db, db) end -local function get_db_property(command, property) +--------------------------------------------------------------------------- +-- get API +--------------------------------------------------------------------------- + +local function get_db_property(entry_name, property) ensure_db() - if not db[command] then - error(('command not found: "%s"'):format(command)) + if not db[entry_name] then + error(('entry not found: "%s"'):format(entry_name)) end - return db[command][property] + return db[entry_name][property] end -- returns the ~54 char summary blurb associated with the entry @@ -308,6 +413,32 @@ function get_entry_tags(entry) return set_to_sorted_list(get_db_property(entry, 'tags')) end +-- returns whether the given string matches a tag name +function is_tag(str) + ensure_db() + return not not tag_index[str] +end + +-- returns the defined tags in alphabetical order +function get_tags() + ensure_db() + return set_to_sorted_list(tag_index) +end + +-- returns the description associated with the given tag +function get_tag_description(tag) + ensure_db() + if not tag_index[tag] then + error('invalid tag: ' .. tag) + end + return tag_index[tag].description +end + +--------------------------------------------------------------------------- +-- search API +--------------------------------------------------------------------------- + +-- returns a list of path elements in reverse order local function chunk_for_sorting(str) local parts = str:split('/') local chunks = {} @@ -336,11 +467,8 @@ local function sort_by_basename(a, b) return false end -local function matches(command, filter) - local db_entry = db[command] - if filter.runnable and db_entry.unrunnable then - return false - end +local function matches(entry_name, filter) + local db_entry = db[entry_name] if filter.tag then local matched = false for _,tag in ipairs(filter.tag) do @@ -353,10 +481,22 @@ local function matches(command, filter) return false end end + if filter.types then + local matched = false + for _,etype in ipairs(filter.types) do + if db_entry.entry_types[etype] then + matched = true + break + end + end + if not matched then + return false + end + end if filter.str then local matched = false for _,str in ipairs(filter.str) do - if command:find(str, 1, true) then + if entry_name:find(str, 1, true) then matched = true break end @@ -368,6 +508,7 @@ local function matches(command, filter) return true end +-- converts strings into single-element lists containing that string local function normalize_string_list(l) if not l then return nil end if type(l) == 'string' then @@ -376,28 +517,35 @@ local function normalize_string_list(l) return l end +-- normalizes the lists in the filter and returns nil if no filter elements are +-- populated local function normalize_filter(f) if not f then return nil end local filter = {} filter.str = normalize_string_list(f.str) filter.tag = normalize_string_list(f.tag) - filter.runnable = f.runnable - if not filter.str and not filter.tag and not filter.runnable then + filter.types = normalize_string_list(f.types) + if not filter.str and not filter.tag and not filter.types then return nil end return filter end --- returns a list of identifiers, alphabetized by their last path component --- (e.g. gui/autobutcher will immediately follow autobutcher). +-- returns a list of entry names, alphabetized by their last path component, +-- with populated path components coming before null path components (e.g. +-- autobutcher will immediately follow gui/autobutcher). -- the optional include and exclude filter params are maps with the following -- elements: -- str - if a string, filters by the given substring. if a table of strings, --- includes commands that match any of the given substrings. +-- includes entry names that match any of the given substrings. -- tag - if a string, filters by the given tag name. if a table of strings, --- includes commands that match any of the given tags. --- runnable - if true, matches only runnable commands, not plugin names. -function get_entries(include, exclude) +-- includes entries that match any of the given tags. +-- types - if a string, matches entries of the given type. if a table of +-- strings, includes entries that match any of the given types. valid +-- types are: "builtin", "plugin", "command". note that many plugin +-- commands have the same name as the plugin, so those entries will +-- match both "plugin" and "command" types. +function search_entries(include, exclude) ensure_db() include = normalize_filter(include) exclude = normalize_filter(exclude) @@ -412,6 +560,10 @@ function get_entries(include, exclude) return commands end +--------------------------------------------------------------------------- +-- list API (outputs to console) +--------------------------------------------------------------------------- + local function get_max_width(list, min_width) local width = min_width or 0 for _,item in ipairs(list) do @@ -420,43 +572,52 @@ local function get_max_width(list, min_width) return width end +-- prints the defined tags and their descriptions to the console +function list_tags() + local tags = get_tags() + local width = get_max_width(tags, 10) + for _,tag in ipairs(tags) do + print((' %-'..width..'s %s'):format(tag, get_tag_description(tag))) + end +end + -- prints the requested entries to the console. include and exclude filters are --- as in get_entries above. -function list_entries(include_tags, include, exclude) - local entries = get_entries(include, exclude) +-- defined as in search_entries() above. +function list_entries(skip_tags, include, exclude) + local entries = search_entries(include, exclude) local width = get_max_width(entries, 10) for _,entry in ipairs(entries) do - print((' %-'..width..'s %s'):format( + print((' %-'..width..'s %s'):format( entry, get_entry_short_help(entry))) - if include_tags then - print((' '..(' '):rep(width)..' tags(%s)'):format( - table.concat(get_entry_tags(entry), ','))) + if not skip_tags then + local tags = get_entry_tags(entry) + if #tags > 0 then + print((' tags: %s'):format(table.concat(tags, ', '))) + end end end -end - --- returns the defined tags in alphabetical order -function get_tags() - ensure_db() - return set_to_sorted_list(tag_index) -end - --- returns the description associated with the given tag -function get_tag_description(tag) - ensure_db() - if not tag_index[tag] then - error('invalid tag: ' .. tag) + if #entries == 0 then + print('no entries found.') end - return tag_index[tag].description end --- prints the defined tags and their descriptions to the console -function list_tags() - local tags = get_tags() - local width = get_max_width(tags, 10) - for _,tag in ipairs(tags) do - print((' %-'..width..'s %s'):format(tag, get_tag_description(tag))) +-- wraps the list_entries() API to provide a more convenient interface for Core +-- to implement the 'ls' builtin command. +-- filter_str - if a tag name, will filter by that tag. otherwise, will filter +-- as a substring +-- skip_tags - whether to skip printing tag info +-- show_dev_commands - if true, will include scripts in the modtools/ and +-- devel/ directories. otherwise those scripts will be +-- excluded +function ls(filter_str, skip_tags, show_dev_commands) + local include = {types={ENTRY_TYPES.COMMAND}} + if is_tag(filter_str) then + include.tag = filter_str + else + include.str = filter_str end + list_entries(skip_tags, include, + show_dev_commands and {} or {str={'modtools/', 'devel/'}}) end return _ENV