diff --git a/control-panel.lua b/control-panel.lua new file mode 100644 index 0000000000..2f4574fb78 --- /dev/null +++ b/control-panel.lua @@ -0,0 +1,231 @@ +--@module = true + +local argparse = require('argparse') +local common = reqscript('internal/control-panel/common') +local json = require('json') +local persist = require('persist-table') +local registry = reqscript('internal/control-panel/registry') +local utils = require('utils') + +local GLOBAL_KEY = 'control-panel' + +-- state change hooks + +local function apply_system_config() + local enabled_map =common.get_enabled_map() + for _, data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'system_enable' then + common.apply_command(data, enabled_map) + end + end + for _, data in ipairs(registry.PREFERENCES_BY_IDX) do + local value = safe_index(common.config.data.preferences, data.name, 'val') + if value ~= nil then + data.set_fn(value) + end + end +end + +local function apply_autostart_config() + local enabled_map =common.get_enabled_map() + for _, data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'enable' or data.mode == 'run' then + common.apply_command(data, enabled_map) + end + end +end + +local function apply_fort_loaded_config() + if not safe_index(json.decode(persist.GlobalTable[GLOBAL_KEY] or ''), 'autostart_done') then + apply_autostart_config() + persist.GlobalTable[GLOBAL_KEY] = json.encode({autostart_done=true}) + end + local enabled_repeats = json.decode(persist.GlobalTable[common.REPEATS_GLOBAL_KEY] or '') + for _, data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'repeat' and enabled_repeats[data.command] then + common.apply_command(data) + end + end +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_CORE_INITIALIZED then + apply_system_config() + elseif sc == SC_MAP_LOADED and dfhack.world.isFortressMode() then + apply_fort_loaded_config() + end +end + + +-- CLI + +local function print_header(header) + print() + print(header) + print(('-'):rep(#header)) +end + +local function list_command_group(group, filter_strs, enabled_map) + local header = ('Group: %s'):format(group) + for idx, data in ipairs(registry.COMMANDS_BY_IDX) do + local first_word = common.get_first_word(data.command) + if not common.command_passes_filters(data, group, first_word, filter_strs) then + goto continue + end + if header then + print_header(header) + ---@diagnostic disable-next-line: cast-local-type + header = nil + end + local extra = '' + if data.mode == 'system_enable' then + extra = ' (global)' + end + print(('%d) %s%s'):format(idx, data.command, extra)) + local desc = common.get_description(data) + if #desc > 0 then + print((' %s'):format(desc)) + end + local default_value = not not data.default + local current_value = safe_index(common.config.data.commands, data.command, 'autostart') + if current_value == nil then + current_value = default_value + end + print((' autostart enabled: %s (default: %s)'):format(current_value, default_value)) + if enabled_map[data.command] ~= nil then + print((' currently enabled: %s'):format(enabled_map[data.command])) + end + print() + ::continue:: + end + if not header then + end +end + +local function list_preferences(filter_strs) + local header = 'Preferences' + for _, data in ipairs(registry.PREFERENCES_BY_IDX) do + local search_key = ('%s %s %s'):format(data.name, data.label, data.desc) + if not utils.search_text(search_key, filter_strs) then goto continue end + if header then + print_header(header) + ---@diagnostic disable-next-line: cast-local-type + header = nil + end + print(('%s) %s'):format(data.name, data.label)) + print((' %s'):format(data.desc)) + print((' current: %s (default: %s)'):format(data.get_fn(), data.default)) + if data.min then + print((' minimum: %s'):format(data.min)) + end + print() + ::continue:: + end +end + +local function do_list(filter_strs) + local enabled_map =common.get_enabled_map() + list_command_group('automation', filter_strs, enabled_map) + list_command_group('bugfix', filter_strs, enabled_map) + list_command_group('gameplay', filter_strs, enabled_map) + list_preferences(filter_strs) +end + +local function get_command_data(name_or_idx) + if type(name_or_idx) == 'number' then + return registry.COMMANDS_BY_IDX[name_or_idx] + end + return registry.COMMANDS_BY_NAME[name_or_idx] +end + +local function do_enable_disable(which, entries) + local enabled_map =common.get_enabled_map() + for _, entry in ipairs(entries) do + local data = get_command_data(entry) + if data.mode ~= 'system_enable' and not dfhack.world.isFortressMode() then + qerror('must have a loaded fortress to enable '..data.name) + end + if common.apply_command(data, enabled_map, which == 'en') then + print(('%sabled %s'):format(which, entry)) + end + end +end + +local function do_enable(entries) + do_enable_disable('en', entries) +end + +local function do_disable(entries) + do_enable_disable('dis', entries) +end + +local function do_autostart_noautostart(which, entries) + for _, entry in ipairs(entries) do + local data = get_command_data(entry) + if not data then + qerror(('autostart command or index not found: "%s"'):format(entry)) + else + common.set_autostart(data, which == 'en') + print(('%sabled autostart for: %s'):format(which, entry)) + end + end + common.config:write() +end + +local function do_autostart(entries) + do_autostart_noautostart('en', entries) +end + +local function do_noautostart(entries) + do_autostart_noautostart('dis', entries) +end + +local function do_set(params) + local name, value = params[1], params[2] + local data = registry.PREFERENCES_BY_NAME[name] + if not data then + qerror(('preference name not found: "%s"'):format(name)) + end + common.set_preference(data, value) + common.config:write() +end + +local function do_reset(params) + local name = params[1] + local data = registry.PREFERENCES_BY_NAME[name] + if not data then + qerror(('preference name not found: "%s"'):format(name)) + end + common.set_preference(data, data.default) + common.config:write() +end + +local command_switch = { + list=do_list, + enable=do_enable, + disable=do_disable, + autostart=do_autostart, + noautostart=do_noautostart, + set=do_set, + reset=do_reset, +} + +local function main(args) + local help = false + + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() help = true end}, + }) + + local command = table.remove(positionals, 1) + if help or not command or not command_switch[command] then + print(dfhack.script_help()) + return + end + + command_switch[command](positionals) +end + +if not dfhack_flags.module then + main{...} +end diff --git a/docs/control-panel.rst b/docs/control-panel.rst new file mode 100644 index 0000000000..5d73663ff9 --- /dev/null +++ b/docs/control-panel.rst @@ -0,0 +1,69 @@ +control-panel +============= + +.. dfhack-tool:: + :summary: Configure DFHack and manage active DFHack tools. + :tags: dfhack + +This is the commandline interface for configuring DFHack behavior, toggling +which functionality is enabled right now, and setting up which tools are +enabled/run when starting new fortress games. For an in-game +graphical interface, please use `gui/control-panel`. For a commandline +interface for configuring which overlays are enabled, please use `overlay`. + +This interface controls three kinds of configuration: + +1. Tools that are enabled right now. These are DFHack tools that run in the +background, like `autofarm`, or tools that DFHack can run on a repeating +schedule, like the "autoMilk" functionality of `workorder`. Most tools that can +be enabled are saved with your fort, so you can have different tools enabled +for different forts. If a tool is marked "global", however, like +`hide-tutorials`, then enabling it will make it take effect for all games. + +2. Tools or commands that should be auto-enabled or auto-run when you start a +new fortress. In addition to tools that can be "enabled", this includes +commands that you might want to run once just after you embark, such as +commands to configure `autobutcher` or to drain portions of excessively deep +aquifers. + +3. DFHack system preferences, such as whether "Armok" (god-mode) tools are +shown in DFHack lists (including the lists of commands shown by the control +panel) or mouse configuration like how fast you have to click for it to count +as a double click (for example, when maximizing DFHack tool windows). +Preferences are "global" in that they apply to all games. + +Run ``control-panel list`` to see the current settings and what tools and +preferences are available for configuration. + +Usage +----- + +:: + + control-panel list + control-panel enable|disable + control-panel autostart|noautostart + control-panel set + control-panel reset + +Examples +-------- +``control-panel list butcher`` + Shows the current configuration of all commands related to `autobutcher` + (and anything else that includes the text "butcher" in it). +``control-panel enable fix/empty-wheelbarrows`` or ``control-panel enable 25`` + Starts to run `fix/empty-wheelbarrows` periodically to maintain the + usability of your wheelbarrows. In the second version of this command, the + number "25" is used as an example. You'll have to run + ``control-panel list`` to see what number this command is actually listed + as. +``control-panel autostart autofarm`` + Configures `autofarm` to become automatically enabled when you start a new + fort. +``control-panel autostart fix/blood-del`` + Configures `fix/blood-del` to run once when you start a new fort. +``control-panel set HIDE_ARMOK_TOOLS true`` + Enable "mortal mode" and hide "armok" tools in the DFHack UIs. Note that + this will also remove some entries from the ``control-panel list`` output. + Run ``control-panel list`` to see all preference options and their + descriptions. diff --git a/docs/gui/control-panel.rst b/docs/gui/control-panel.rst index a8933087fd..4f3b8608f6 100644 --- a/docs/gui/control-panel.rst +++ b/docs/gui/control-panel.rst @@ -2,83 +2,82 @@ gui/control-panel ================= .. dfhack-tool:: - :summary: Configure DFHack. + :summary: Configure DFHack and manage active DFHack tools. :tags: dfhack The DFHack control panel allows you to quickly see and change what DFHack tools -are enabled now, which tools will run when you start a new fort, and how global -DFHack configuration options are set. It also provides convenient links to -relevant help pages and GUI configuration frontends. The control panel has -several pages that you can switch among by clicking on the tabs at the top of -the window. Each page has a search filter so you can quickly find the tools and -options that you're looking for. - -Fort Services -------------- - -The fort services page shows tools that you can enable in fort mode. You can -select the tool name to see a short description at the bottom of the list. Hit +are enabled, which tools will run when you start a new fort, which UI overlays +are enabled, and how global DFHack configuration options are set. It also +provides convenient links to relevant help pages and GUI configuration +frontends (where available). The control panel has several sections that you +can access by clicking on the tabs at the top of the window. Each tab has a +search filter so you can quickly find the tools and options that you're looking +for. + +The tabs can also be navigated with the keyboard, with the :kbd:`Ctrl`:kbd:`T` +and :kbd:`Ctrl`:kbd:`Y` hotkeys. These are the default hotkeys for navigating +DFHack tab bars. + +The "Enabled" tab +----------------- + +The "Enabled" tab shows tools that you can enable right now. You can select the +tool name to see a short description at the bottom of the list. Hit :kbd:`Enter`, double click on the tool name, or click on the toggle on the far left to enable or disable that tool. -Note that the fort services displayed on this page can only be enabled when a -fort is loaded. They will be disabled in the list and cannot be enabled or have -their GUI config screens shown until you have loaded a fortress. Once you do -enable them (after you've loaded a fort), they will save their state with your -fort and automatically re-enable themselves when you load your fort again. +Note that before a fort is loaded, there will be very few tools listed here. -You can hit :kbd:`Ctrl`:kbd:`H` or click on the help icon to show the help page -for the selected tool in `gui/launcher`. You can also use this as shortcut to +Tools are split into three subcategories: ``automation``, ``bugfix``, and +``gameplay``. In general, you'll probably want to start with only the +``bugfix`` tools enabled. As you become more comfortable with vanilla systems, +and some of them start to become less fun and more toilsome, you can enable +more of the ``automation`` tools to manage them for you. Finally, you can +examine the tools on the ``gameplay`` tab and enable whatever you think sounds +like fun :). + +The category subtabs can also be navigated with the keyboard, with the +:kbd:`Ctrl`:kbd:`N` and :kbd:`Ctrl`:kbd:`M` hotkeys. + +Once tools are enabled (possible after you've loaded a fort), they will save +their state with your fort and automatically re-enable themselves when you load +that same fort again. + +You can hit :kbd:`Ctrl`:kbd:`H` or click on the help icon to show the help page for the selected tool in `gui/launcher`. You can also use this as shortcut to run custom commandline commands to configure that tool manually. If the tool has an associated GUI config screen, a gear icon will also appear next to the help -icon. Hit :kbd:`Ctrl`:kbd:`G` or click on that icon to launch the relevant -configuration interface. +icon. Hit :kbd:`Ctrl`:kbd:`G`, click on the gear icon, or Shift-double click the tool name to launch the relevant configuration interface. .. _dfhack-examples-guide: -New Fort Autostart Commands ---------------------------- - -This page shows the tools that you can configure DFHack to auto-enable or -auto-run when you start a new fort. You'll recognize many tools from the -previous page here, but there are also useful one-time commands that you might -want to run at the start of a fort, like `ban-cooking all `. - -Periodic Maintenance Operations -------------------------------- - -This page shows commands that DFHack can regularly run for you in order to keep -your fort (and the game) running smoothly. For example, there are commands to -periodically enqueue orders for shearing animals that are ready to be shorn or -sort your manager orders so slow-moving daily orders won't prevent your -high-volume one-time orders from ever being completed. - -System Services ---------------- - -The system services page shows "core" DFHack tools that provide background -services to other tools. It is generally not advisable to turn these tools -off. If you do toggle them off in the control panel, they will be re-enabled -when you restart the game. If you really need to turn these tools off -permanently, add a line like ``disable toolname`` to your -``dfhack-config/init/dfhack.init`` file. - -Overlays --------- - -The overlays page allows you to easily see which overlays are enabled and lets -you toggle them on and off and see the help for the owning tools. If you want to -reposition any of the overlay widgets, hit :kbd:`Ctrl`:kbd:`G` or click on -the the hotkey hint to launch `gui/overlay`. - -Preferences ------------ - -The preferences page allows you to change DFHack's internal settings and -defaults, like whether DFHack tools pause the game when they come up, or how -long you can wait between clicks and still have it count as a double-click. Hit -:kbd:`Ctrl`:kbd:`G` or click on the hotkey hint at the bottom of the page to -restore all preferences to defaults. +The "Autostart" tab +------------------- + +This tab is organized similarly to the "Enabled" tab, but instead of tools you +can enable now, it shows the tools that you can configure DFHack to auto-enable +or auto-run when you start the game or a new fort. You'll recognize many tools +from the "Enabled" tab here, but there are also useful one-time commands that +you might want to run at the start of a fort, like +`ban-cooking all `. + +The "UI Overlays" tab +--------------------- + +The overlays tab allows you to easily see which overlays are enabled, lets you +toggle them on and off, and gives you links for the related help text (which is +normally added at the bottom of the help page for the tool that provides the +overlay). If you want to reposition any of the overlay widgets, hit +:kbd:`Ctrl`:kbd:`G` or click on the the hotkey hint to launch `gui/overlay`. + +The "Preferences" tab +--------------------- + +The preferences tab allows you to change DFHack's internal settings and +defaults, like whether DFHack's "mortal mode" is enabled -- hiding the god-mode +tools from the UI, whether DFHack tools pause the game when they come up, or how +long you can take between clicks and still have it count as a double-click. +Click on the gear icon or hit :kbd:`Enter` to toggle or edit the selected +preference. Usage ----- diff --git a/gui/control-panel.lua b/gui/control-panel.lua index d90c617c7b..262de23c01 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -1,167 +1,13 @@ +local common = reqscript('internal/control-panel/common') local dialogs = require('gui.dialogs') local gui = require('gui') -local textures = require('gui.textures') local helpdb = require('helpdb') +local textures = require('gui.textures') local overlay = require('plugins.overlay') -local repeatUtil = require('repeat-util') +local registry = reqscript('internal/control-panel/registry') local utils = require('utils') local widgets = require('gui.widgets') --- init files -local SYSTEM_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-system.init' -local PREFERENCES_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-preferences.init' -local AUTOSTART_FILE = 'dfhack-config/init/onMapLoad.control-panel-new-fort.init' -local REPEATS_FILE = 'dfhack-config/init/onMapLoad.control-panel-repeats.init' - --- service and command lists -local FORT_SERVICES = { - 'autobutcher', - 'autochop', - 'autoclothing', - 'autofarm', - 'autofish', - 'autonestbox', - 'autoslab', - 'dwarfvet', - 'emigration', - 'fastdwarf', - 'fix/protect-nicks', - 'hermit', - 'misery', - 'nestboxes', - 'preserve-tombs', - 'prioritize', - 'seedwatch', - 'starvingdead', - 'suspendmanager', - 'tailor', -} - -local FORT_AUTOSTART = { - 'autobutcher target 10 10 14 2 BIRD_GOOSE', - 'autobutcher target 10 10 14 2 BIRD_TURKEY', - 'autobutcher target 10 10 14 2 BIRD_CHICKEN', - 'autofarm threshold 150 grass_tail_pig', - 'ban-cooking all', - 'buildingplan set boulders false', - 'buildingplan set logs false', - 'drain-aquifer --top 2', - 'fix/blood-del fort', - 'light-aquifers-only fort', -} -for _,v in ipairs(FORT_SERVICES) do - table.insert(FORT_AUTOSTART, v) -end -table.sort(FORT_AUTOSTART) - --- these are re-enabled by the default DFHack init scripts -local SYSTEM_SERVICES = { -} --- these are fully controlled by the user -local SYSTEM_USER_SERVICES = { - 'faststart', - 'hide-tutorials', - 'work-now', -} -for _,v in ipairs(SYSTEM_USER_SERVICES) do - table.insert(SYSTEM_SERVICES, v) -end -table.sort(SYSTEM_SERVICES) - -local PREFERENCES = { - ['dfhack']={ - HIDE_CONSOLE_ON_STARTUP={label='Hide console on startup (MS Windows only)', type='bool', default=true, - desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.'}, - HIDE_ARMOK_TOOLS={label='Mortal mode: hide "armok" tools', type='bool', default=false, - desc='Don\'t show tools that give you god-like powers wherever DFHack tools are listed.'}, - }, - ['gui']={ - DEFAULT_INITIAL_PAUSE={label='DFHack tools autopause game', type='bool', default=true, - desc='Whether to pause the game when a DFHack tool window is shown.'}, - }, - ['gui.widgets']={ - DOUBLE_CLICK_MS={label='Mouse double click speed (ms)', type='int', default=500, min=50, - desc='How long to wait for the second click of a double click, in ms.'}, - SCROLL_INITIAL_DELAY_MS={label='Mouse initial scroll repeat delay (ms)', type='int', default=300, min=5, - desc='The delay before scrolling quickly when holding the mouse button down on a scrollbar, in ms.'}, - SCROLL_DELAY_MS={label='Mouse scroll repeat delay (ms)', type='int', default=20, min=5, - desc='The delay between events when holding the mouse button down on a scrollbar, in ms.'}, - }, - ['utils']={ - FILTER_FULL_TEXT={label='DFHack searches full text', type='bool', default=false, - desc='When searching, choose whether to match anywhere in the text (true) or just at the start of words (false).'}, - }, -} -local CPP_PREFERENCES = { - { - label='Prevent duplicate key events', - type='bool', - default=true, - desc='Whether to additionally pass key events through to DF when DFHack keybindings are triggered.', - init_fmt=':lua dfhack.internal.setSuppressDuplicateKeyboardEvents(%s)', - get_fn=dfhack.internal.getSuppressDuplicateKeyboardEvents, - set_fn=dfhack.internal.setSuppressDuplicateKeyboardEvents, - }, -} - -local REPEATS = { - ['autoMilkCreature']={ - desc='Automatically milk creatures that are ready for milking.', - command={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', '"{\\"job\\":\\"MilkCreature\\",\\"item_conditions\\":[{\\"condition\\":\\"AtLeast\\",\\"value\\":2,\\"flags\\":[\\"empty\\"],\\"item_type\\":\\"BUCKET\\"}]}"', ']'}}, - ['autoShearCreature']={ - desc='Automatically shear creatures that are ready for shearing.', - command={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', 'ShearCreature', ']'}}, - ['cleanowned']={ - desc='Encourage dwarves to drop tattered clothing and grab new ones.', - command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, - ['combine']={ - desc='Combine partial stacks in stockpiles into full stacks.', - command={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, - ['dead-units-burrow']={ - desc='Fix units still being assigned to burrows after death.', - command={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, - ['empty-wheelbarrows']={ - desc='Empties wheelbarrows which have rocks stuck in them.', - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, - ['general-strike']={ - desc='Prevent dwarves from getting stuck and refusing to work.', - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/general-strike', '-q', ']'}}, - ['orders-sort']={ - desc='Sort manager orders by repeat frequency so one-time orders can be completed.', - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, - ['orders-reevaluate']={ - desc='Invalidates work orders once a month, allowing conditions to be rechecked.', - command={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, - ['stuck-instruments']={ - desc='Fix activity references on stuck instruments to make them usable again.', - command={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, - ['warn-starving']={ - desc='Show a warning dialog when units are starving or dehydrated.', - command={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, - ['warn-stranded']={ - desc='Show a warning dialog when units are stranded from all others.', - command={'--time', '2', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, -} -local REPEATS_LIST = {} -for k in pairs(REPEATS) do - table.insert(REPEATS_LIST, k) -end -table.sort(REPEATS_LIST) - --- save_fn takes the file as a param and should call f:write() to write data -local function save_file(path, save_fn) - local ok, f = pcall(io.open, path, 'w') - if not ok or not f then - dialogs.showMessage('Error', - ('Cannot open file for writing: "%s"'):format(path)) - return - end - f:write('# DO NOT EDIT THIS FILE\n') - f:write('# Please use gui/control-panel to edit this file\n\n') - save_fn(f) - f:close() -end - local function get_icon_pens() local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} @@ -197,122 +43,268 @@ local ENABLED_PEN_LEFT, ENABLED_PEN_CENTER, ENABLED_PEN_RIGHT, -- ConfigPanel -- +-- provides common structure across control panel tabs ConfigPanel = defclass(ConfigPanel, widgets.Panel) ConfigPanel.ATTRS{ intro_text=DEFAULT_NIL, - is_enableable=DEFAULT_NIL, - is_configurable=DEFAULT_NIL, - select_label='Toggle enabled', } function ConfigPanel:init() - self:addviews{ - widgets.Panel{ - frame={t=0, b=7}, - autoarrange_subviews=true, - autoarrange_gap=1, - subviews={ - widgets.WrappedLabel{ - frame={t=0}, - text_to_wrap=self.intro_text, - }, - widgets.FilteredList{ - frame={t=5}, - view_id='list', - on_select=self:callback('on_select'), - on_double_click=self:callback('on_submit'), - on_double_click2=self:callback('launch_config'), - row_height=2, - }, + local main_panel = widgets.Panel{ + frame={t=0, b=7}, + autoarrange_subviews=true, + autoarrange_gap=1, + subviews={ + widgets.WrappedLabel{ + frame={t=0}, + text_to_wrap=self.intro_text, }, + -- extended by subclasses }, + } + self:init_main_panel(main_panel) + + local footer = widgets.Panel{ + view_id='footer', + frame={b=0, h=3}, + subviews={ + widgets.HotkeyLabel{ + frame={t=2, l=0}, + label='Restore defaults', + key='CUSTOM_CTRL_D', + auto_width=true, + on_activate=self:callback('restore_defaults') + }, + -- extended by subclasses + } + } + self:init_footer(footer) + + self:addviews{ + main_panel, widgets.WrappedLabel{ view_id='desc', frame={b=4, h=2}, auto_height=false, + text_to_wrap='', -- updated in on_select + }, + footer, + } +end + +-- overridden by subclasses +function ConfigPanel:init_main_panel(panel) +end + +-- overridden by subclasses +function ConfigPanel:init_footer(panel) +end + +-- overridden by subclasses +function ConfigPanel:refresh() +end + +-- overridden by subclasses +function ConfigPanel:restore_defaults() +end + +-- attach to lists in subclasses +-- choice.data is an entry from one of the registry tables +function ConfigPanel:on_select(_, choice) + local desc = self.subviews.desc + desc.text_to_wrap = choice and common.get_description(choice.data) or '' + if desc.frame_body then + desc:updateLayout() + end +end + + +-- +-- CommandTab +-- + +CommandTab = defclass(CommandTab, ConfigPanel) + +local Subtabs = { + automation=1, + bugfix=2, + gameplay=3, +} +local Subtabs_revmap = utils.invert(Subtabs) + +function CommandTab:init() + self.subpage = Subtabs.automation + + self.blurbs = { + [Subtabs.automation]='These run in the background and help you manage your'.. + ' fort. They are always safe to enable, and allow you to concentrate on'.. + ' other aspects of gameplay that you find more enjoyable.', + [Subtabs.bugfix]='These automatically fix dangerous or annoying vanilla'.. + ' bugs. You should generally have all of these enabled.', + [Subtabs.gameplay]='These change or extend gameplay. Read their help docs to'.. + ' see what they do and enable the ones that appeal to you.', + } +end + +function CommandTab:init_main_panel(panel) + panel:addviews{ + widgets.TabBar{ + frame={t=5}, + key='CUSTOM_CTRL_N', + key_back='CUSTOM_CTRL_M', + labels={ + 'Automation', + 'Bug Fixes', + 'Gameplay', + }, + on_select=function(val) + self.subpage = val + self:updateLayout() + self:refresh() + end, + get_cur_page=function() return self.subpage end, + }, + widgets.WrappedLabel{ + frame={t=7}, + text_to_wrap=function() return self.blurbs[self.subpage] end, + }, + widgets.FilteredList{ + frame={t=9}, + view_id='list', + on_select=self:callback('on_select'), + on_double_click=self:callback('on_submit'), + row_height=2, }, + } +end + +function CommandTab:init_footer(panel) + panel:addviews{ widgets.HotkeyLabel{ - frame={b=2, l=0}, - label=self.select_label, + frame={t=0, l=0}, + label='Toggle enabled', key='SELECT', - enabled=self.is_enableable, + auto_width=true, on_activate=self:callback('on_submit') }, widgets.HotkeyLabel{ - view_id='show_help_label', - frame={b=1, l=0}, - label='Show tool help or run commands', + frame={t=1, l=0}, + label='Show full tool help or run custom command', + auto_width=true, key='CUSTOM_CTRL_H', - on_activate=self:callback('show_help') + on_activate=self:callback('show_help'), }, + } +end + +local function launch_help(command) + dfhack.run_command('gui/launcher', command .. ' ') +end + +function CommandTab:show_help() + _,choice = self.subviews.list:getSelected() + if not choice then return end + launch_help(choice.data.command) +end + +function CommandTab:has_config() + _,choice = self.subviews.list:getSelected() + return choice and choice.gui_config +end + + +-- +-- EnabledTab +-- + +EnabledTab = defclass(EnabledTab, CommandTab) +EnabledTab.ATTRS{ + intro_text='These are the tools that can be enabled right now. Most tools can'.. + ' only be enabled when you have a fort loaded. Once enabled, tools'.. + ' will stay enabled when you save and reload your fort. If you want'.. + ' them to be auto-enabled for new forts, please see the "Autostart"'.. + ' tab.', +} + +function EnabledTab:init() + if not dfhack.world.isFortressMode() then + self.subpage = Subtabs.gameplay + end + + self.subviews.list.list.on_double_click2=self:callback('launch_config') +end + +function EnabledTab:init_footer(panel) + EnabledTab.super.init_footer(self, panel) + panel:addviews{ widgets.HotkeyLabel{ - view_id='launch', - frame={b=0, l=0}, - label='Launch config UI', + frame={t=2, l=26}, + label='Launch tool-specific config UI', key='CUSTOM_CTRL_G', - enabled=self.is_configurable, + auto_width=true, + enabled=self:callback('has_config'), on_activate=self:callback('launch_config'), }, } end -function ConfigPanel:onInput(keys) - local handled = ConfigPanel.super.onInput(self, keys) - if keys._MOUSE_L then - local list = self.subviews.list.list - local idx = list:getIdxUnderMouse() - if idx then - local x = list:getMousePos() - if x <= 2 then - self:on_submit() - elseif x >= 4 and x <= 6 then - self:show_help() - elseif x >= 8 and x <= 10 then - self:launch_config() - end - end +local function get_gui_config(command) + command = common.get_first_word(command) + local gui_config = 'gui/' .. command + if helpdb.is_entry(gui_config) then + return gui_config end - return handled end -local COMMAND_REGEX = '^([%w/_-]+)' +local function make_enabled_text(label, mode, enabled, gui_config) + if mode == 'system_enable' then + label = label .. ' (global)' + end -function ConfigPanel:refresh() + local function get_config_button_token(tile) + return { + tile=gui_config and tile or nil, + text=not gui_config and ' ' or nil, + } + end + + return { + {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, + {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, + {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, + ' ', + {tile=BUTTON_PEN_LEFT}, + {tile=HELP_PEN_CENTER}, + {tile=BUTTON_PEN_RIGHT}, + ' ', + get_config_button_token(BUTTON_PEN_LEFT), + get_config_button_token(CONFIGURE_PEN_CENTER), + get_config_button_token(BUTTON_PEN_RIGHT), + ' ', + label, + } +end + +function EnabledTab:refresh() local choices = {} - for _,choice in ipairs(self:get_choices()) do - local command = choice.target or choice.command - command = command:match(COMMAND_REGEX) - local gui_config = 'gui/' .. command - local want_gui_config = utils.getval(self.is_configurable, gui_config) - and helpdb.is_entry(gui_config) - local enabled = choice.enabled - local function get_enabled_pen(enabled_pen, disabled_pen) - if not utils.getval(self.is_enableable) then - return gui.CLEAR_PEN - end - return enabled and enabled_pen or disabled_pen + self.enabled_map = common.get_enabled_map() + local group = Subtabs_revmap[self.subpage] + for _,data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'run' then goto continue end + if data.mode ~= 'system_enable' and not dfhack.world.isFortressMode() then + goto continue end - local text = { - {tile=get_enabled_pen(ENABLED_PEN_LEFT, DISABLED_PEN_LEFT)}, - {tile=get_enabled_pen(ENABLED_PEN_CENTER, DISABLED_PEN_CENTER)}, - {tile=get_enabled_pen(ENABLED_PEN_RIGHT, DISABLED_PEN_RIGHT)}, - ' ', - {tile=BUTTON_PEN_LEFT}, - {tile=HELP_PEN_CENTER}, - {tile=BUTTON_PEN_RIGHT}, - ' ', - {tile=want_gui_config and BUTTON_PEN_LEFT or gui.CLEAR_PEN}, - {tile=want_gui_config and CONFIGURE_PEN_CENTER or gui.CLEAR_PEN}, - {tile=want_gui_config and BUTTON_PEN_RIGHT or gui.CLEAR_PEN}, - ' ', - choice.target, - } - local desc = helpdb.is_entry(command) and - helpdb.get_entry_short_help(command) or '' - table.insert(choices, - {text=text, command=choice.command, target=choice.target, desc=desc, - search_key=choice.target, enabled=enabled, - gui_config=want_gui_config and gui_config}) + if not common.command_passes_filters(data, group) then goto continue end + local enabled = self.enabled_map[data.command] + local gui_config = get_gui_config(data.command) + table.insert(choices, { + text=make_enabled_text(data.command, data.mode, enabled, gui_config), + search_key=data.command, + data=data, + enabled=enabled, + gui_config=gui_config, + }) + ::continue:: end local list = self.subviews.list local filter = list:getFilter() @@ -322,236 +314,297 @@ function ConfigPanel:refresh() list.edit:setFocus(true) end -function ConfigPanel:on_select(idx, choice) - local desc = self.subviews.desc - desc.text_to_wrap = choice and choice.desc or '' - if desc.frame_body then - desc:updateLayout() - end - if choice then - self.subviews.launch.enabled = utils.getval(self.is_configurable) - and not not choice.gui_config +function EnabledTab:onInput(keys) + local handled = EnabledTab.super.onInput(self, keys) + if keys._MOUSE_L then + local list = self.subviews.list.list + local idx = list:getIdxUnderMouse() + if idx then + local x = list:getMousePos() + if x <= 2 then + self:on_submit() + elseif x >= 4 and x <= 6 then + self:show_help() + elseif x >= 8 and x <= 10 then + self:launch_config() + end + end end + return handled end -function ConfigPanel:on_submit() - if not utils.getval(self.is_enableable) then return false end +function EnabledTab:launch_config() _,choice = self.subviews.list:getSelected() - if not choice then return end - local tokens = {} - table.insert(tokens, choice.command) - table.insert(tokens, choice.enabled and 'disable' or 'enable') - table.insert(tokens, choice.target) - dfhack.run_command(tokens) - self:refresh() + if not choice or not choice.gui_config then return end + dfhack.run_command(choice.gui_config) end -function ConfigPanel:show_help() +function EnabledTab:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end - local command = choice.target:match(COMMAND_REGEX) - dfhack.run_command('gui/launcher', command .. ' ') + local data = choice.data + common.apply_command(data, self.enabled_map, not choice.enabled) + self:refresh() end -function ConfigPanel:launch_config() - if not utils.getval(self.is_configurable) then return false end - _,choice = self.subviews.list:getSelected() - if not choice or not choice.gui_config then return end - dfhack.run_command(choice.gui_config) +function EnabledTab:restore_defaults() + local group = Subtabs_revmap[self.subpage] + for _,data in ipairs(registry.COMMANDS_BY_IDX) do + if data.mode == 'run' then goto continue end + if (data.mode == 'enable' or data.mode == 'repeat') + and not dfhack.world.isFortressMode() + then + goto continue + end + if not common.command_passes_filters(data, group) then goto continue end + common.apply_command(data, self.enabled_map, data.default) + ::continue:: + end + self:refresh() + dialogs.showMessage('Success', + ('Enabled defaults restored for %s tools.'):format(group)) end + -- --- Services +-- AutostartTab -- -Services = defclass(Services, ConfigPanel) -Services.ATTRS{ - services_list=DEFAULT_NIL, +AutostartTab = defclass(AutostartTab, CommandTab) +AutostartTab.ATTRS{ + intro_text='Tools that are enabled on this page will be auto-run or auto-enabled'.. + ' for you when you start a new fort (or, for "global" tools, when you start the game). To see tools that are enabled'.. + ' right now, please click on the "Enabled" tab.', } -function Services:get_enabled_map() - local enabled_map = {} - local output = dfhack.run_command_silent('enable'):split('\n+') - for _,line in ipairs(output) do - local _,_,command,enabled_str,extra = line:find('%s*(%S+):%s+(%S+)%s*(.*)') - if enabled_str then - enabled_map[command] = enabled_str == 'on' - end +local function make_autostart_text(label, mode, enabled) + if mode == 'system_enable' then + label = label .. ' (global)' end - return enabled_map -end - -local function get_first_word(text) - local word = text:trim():split(' +')[1] - if word:startswith(':') then word = word:sub(2) end - return word + return { + {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, + {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, + {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, + ' ', + {tile=BUTTON_PEN_LEFT}, + {tile=HELP_PEN_CENTER}, + {tile=BUTTON_PEN_RIGHT}, + ' ', + label, + } end -function Services:get_choices() - local enabled_map = self:get_enabled_map() +function AutostartTab:refresh() local choices = {} - local hide_armok = dfhack.getHideArmokTools() - for _,service in ipairs(self.services_list) do - local entry_name = get_first_word(service) - if not hide_armok or not helpdb.is_entry(entry_name) - or not helpdb.get_entry_tags(entry_name).armok then - table.insert(choices, {target=service, enabled=enabled_map[service]}) + local group = Subtabs_revmap[self.subpage] + for _,data in ipairs(registry.COMMANDS_BY_IDX) do + if not common.command_passes_filters(data, group) then goto continue end + local enabled = safe_index(common.config.data.commands, data.command, 'autostart') + if enabled == nil then + enabled = data.default end + table.insert(choices, { + text=make_autostart_text(data.command, data.mode, enabled), + search_key=data.command, + data=data, + enabled=enabled, + }) + ::continue:: end - return choices + local list = self.subviews.list + local filter = list:getFilter() + local selected = list:getSelected() + list:setChoices(choices) + list:setFilter(filter, selected) + list.edit:setFocus(true) end --- --- FortServices --- - -FortServices = defclass(FortServices, Services) -FortServices.ATTRS{ - is_enableable=dfhack.world.isFortressMode, - is_configurable=function() return dfhack.world.isFortressMode() end, - intro_text='These tools can only be enabled when you have a fort loaded,'.. - ' but once you enable them, they will stay enabled when you'.. - ' save and reload your fort. If you want them to be'.. - ' auto-enabled for new forts, please see the "Autostart" tab.', - services_list=FORT_SERVICES, -} - --- --- FortServicesAutostart --- - -FortServicesAutostart = defclass(FortServicesAutostart, Services) -FortServicesAutostart.ATTRS{ - is_enableable=true, - is_configurable=false, - intro_text='Tools that are enabled on this page will be auto-enabled for'.. - ' you when you start a new fort, using the default'.. - ' configuration. To see tools that are enabled right now in'.. - ' an active fort, please see the "Fort" tab.', - services_list=FORT_AUTOSTART, -} - -function FortServicesAutostart:init() - local enabled_map = {} - local ok, f = pcall(io.open, AUTOSTART_FILE) - if ok and f then - local services_set = utils.invert(FORT_AUTOSTART) - for line in f:lines() do - line = line:trim() - if #line == 0 or line:startswith('#') then goto continue end - local service = line:match('^on%-new%-fortress enable ([%S]+)$') - or line:match('^on%-new%-fortress (.+)') - if service and services_set[service] then - enabled_map[service] = true +function AutostartTab:onInput(keys) + local handled = EnabledTab.super.onInput(self, keys) + if keys._MOUSE_L then + local list = self.subviews.list.list + local idx = list:getIdxUnderMouse() + if idx then + local x = list:getMousePos() + if x <= 2 then + self:on_submit() + elseif x >= 4 and x <= 6 then + self:show_help() end - ::continue:: end end - self.enabled_map = enabled_map -end - -function FortServicesAutostart:get_enabled_map() - return self.enabled_map + return handled end -function FortServicesAutostart:on_submit() +function AutostartTab:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end - self.enabled_map[choice.target] = not choice.enabled - - local save_fn = function(f) - for service,enabled in pairs(self.enabled_map) do - if enabled then - if service:match(' ') then - f:write(('on-new-fortress %s\n'):format(service)) - else - f:write(('on-new-fortress enable %s\n'):format(service)) - end - end - end - end - save_file(AUTOSTART_FILE, save_fn) + local data = choice.data + common.set_autostart(data, not choice.enabled) + common.config:write() self:refresh() end --- --- SystemServices --- - -local function system_service_is_configurable(gui_config) - return gui_config ~= 'gui/automelt' or dfhack.world.isFortressMode() -end - -SystemServices = defclass(SystemServices, Services) -SystemServices.ATTRS{ - title='System', - is_enableable=true, - is_configurable=system_service_is_configurable, - intro_text='These are DFHack system services that are not bound to' .. - ' a specific fort. Some of these are critical DFHack services' .. - ' that can be manually disabled, but will re-enable themselves' .. - ' when DF restarts.', - services_list=SYSTEM_SERVICES, -} - -function SystemServices:on_submit() - SystemServices.super.on_submit(self) - - local enabled_map = self:get_enabled_map() - local save_fn = function(f) - for _,service in ipairs(SYSTEM_USER_SERVICES) do - if enabled_map[service] then - f:write(('enable %s\n'):format(service)) - end - end +function AutostartTab:restore_defaults() + local group = Subtabs_revmap[self.subpage] + for _,data in ipairs(registry.COMMANDS_BY_IDX) do + if not common.command_passes_filters(data, group) then goto continue end + print(data.command, data.default) + common.set_autostart(data, data.default) + ::continue:: end - save_file(SYSTEM_INIT_FILE, save_fn) + common.config:write() + self:refresh() + dialogs.showMessage('Success', + ('Autostart defaults restored for %s tools.'):format(group)) end + -- --- Overlays +-- OverlaysTab -- -Overlays = defclass(Overlays, ConfigPanel) -Overlays.ATTRS{ - title='Overlays', - is_enableable=true, - is_configurable=false, +OverlaysTab = defclass(OverlaysTab, ConfigPanel) +OverlaysTab.ATTRS{ intro_text='These are DFHack overlays that add information and'.. - ' functionality to various DF screens.', + ' functionality to native DF screens. You can toggle whether'.. + ' they are enabled here, or you can reposition them with'.. + ' gui/overlay.', } -function Overlays:init() - self.subviews.launch.visible = false - self:addviews{ +function OverlaysTab:init_main_panel(panel) + panel:addviews{ + widgets.FilteredList{ + frame={t=5}, + view_id='list', + on_select=self:callback('on_select'), + on_double_click=self:callback('on_submit'), + row_height=2, + }, + } +end + +function OverlaysTab:init_footer(panel) + panel:addviews{ widgets.HotkeyLabel{ - frame={b=0, l=0}, - label='Launch overlay widget repositioning UI', + frame={t=0, l=0}, + label='Toggle overlay', + key='SELECT', + auto_width=true, + on_activate=self:callback('on_submit') + }, + widgets.HotkeyLabel{ + frame={t=1, l=0}, + label='Show overlay help', + auto_width=true, + key='CUSTOM_CTRL_H', + on_activate=self:callback('show_help'), + }, + widgets.HotkeyLabel{ + frame={t=2, l=26}, + label='Launch widget position adjustment UI', key='CUSTOM_CTRL_G', + auto_width=true, on_activate=function() dfhack.run_script('gui/overlay') end, }, } end -function Overlays:get_choices() +function OverlaysTab:onInput(keys) + local handled = OverlaysTab.super.onInput(self, keys) + if keys._MOUSE_L then + local list = self.subviews.list.list + local idx = list:getIdxUnderMouse() + if idx then + local x = list:getMousePos() + if x <= 2 then + self:on_submit() + elseif x >= 4 and x <= 6 then + self:show_help() + end + end + end + return handled +end + +local function make_overlay_text(label, enabled) + return { + {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, + {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, + {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, + ' ', + {tile=BUTTON_PEN_LEFT}, + {tile=HELP_PEN_CENTER}, + {tile=BUTTON_PEN_RIGHT}, + ' ', + label, + } +end + +function OverlaysTab:refresh() local choices = {} local state = overlay.get_state() for _,name in ipairs(state.index) do - table.insert(choices, {command='overlay', - target=name, - enabled=state.config[name].enabled}) + enabled = state.config[name].enabled + local text = make_overlay_text(name, enabled) + table.insert(choices, { + text=text, + search_key=name, + data={ + name=name, + command=name:match('^(.-)%.') or 'overlay', + desc=state.db[name].widget.desc, + }, + enabled=enabled, + }) + end + local list = self.subviews.list + local filter = list:getFilter() + local selected = list:getSelected() + list:setChoices(choices) + list:setFilter(filter, selected) + list.edit:setFocus(true) +end + +local function enable_overlay(name, enabled) + local tokens = {'overlay'} + table.insert(tokens, enabled and 'enable' or 'disable') + table.insert(tokens, name) + dfhack.run_command(tokens) +end + +function OverlaysTab:on_submit() + _,choice = self.subviews.list:getSelected() + if not choice then return end + local data = choice.data + enable_overlay(data.name, not choice.enabled) + self:refresh() +end + +function OverlaysTab:restore_defaults() + local state = overlay.get_state() + for name, db_entry in pairs(state.db) do + enable_overlay(name, db_entry.widget.default_enabled) end - return choices + self:refresh() + dialogs.showMessage('Success', 'Overlay defaults restored.') +end + +function OverlaysTab:show_help() + _,choice = self.subviews.list:getSelected() + if not choice then return end + launch_help(choice.data.command) end + -- --- Preferences +-- PreferencesTab -- IntegerInputDialog = defclass(IntegerInputDialog, widgets.Window) IntegerInputDialog.ATTRS{ visible=false, - frame={w=50, h=8}, + frame={w=50, h=11}, frame_title='Edit setting', frame_style=gui.PANEL_FRAME, on_hide=DEFAULT_NIL, @@ -562,37 +615,54 @@ function IntegerInputDialog:init() widgets.Label{ frame={t=0, l=0}, text={ - 'Please enter a new value for ', - {text=function() return self.id or '' end}, + 'Please enter a new value for ', NEWLINE, + { + gap=4, + text=function() return self.id or '' end, + }, NEWLINE, {text=self:callback('get_spec_str')}, }, }, widgets.EditField{ view_id='input_edit', - frame={t=3, l=0}, + frame={t=4, l=0}, on_char=function(ch) return ch:match('%d') end, }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + label='Save', + key='SELECT', + auto_width=true, + on_activate=function() self:hide(self.subviews.input_edit.text) end, + }, + widgets.HotkeyLabel{ + frame={b=0, r=0}, + label='Reset to default', + key='CUSTOM_CTRL_D', + auto_width=true, + on_activate=function() self.subviews.input_edit:setText(tostring(self.data.default)) end, + }, } end function IntegerInputDialog:get_spec_str() - if not self.spec or (not self.spec.min and not self.spec.max) then - return '' - end - local strs = {} - if self.spec.min then - table.insert(strs, ('at least %d'):format(self.spec.min)) + local data = self.data + local strs = { + ('default: %d'):format(data.default), + } + if data.min then + table.insert(strs, ('at least %d'):format(data.min)) end - if self.spec.max then - table.insert(strs, ('at most %d'):format(self.spec.max)) + if data.max then + table.insert(strs, ('at most %d'):format(data.max)) end return ('(%s)'):format(table.concat(strs, ', ')) end -function IntegerInputDialog:show(id, spec, initial) +function IntegerInputDialog:show(id, data, initial) self.visible = true - self.id, self.spec = id, spec + self.id, self.data = id, data local edit = self.subviews.input_edit edit:setText(tostring(initial)) edit:setFocus(true) @@ -608,33 +678,25 @@ function IntegerInputDialog:onInput(keys) if IntegerInputDialog.super.onInput(self, keys) then return true end - if keys.SELECT then - self:hide(self.subviews.input_edit.text) - return true - elseif keys.LEAVESCREEN or keys._MOUSE_R then + if keys.LEAVESCREEN or keys._MOUSE_R then self:hide() return true end end -Preferences = defclass(Preferences, ConfigPanel) -Preferences.ATTRS{ - title='Preferences', - is_enableable=true, - is_configurable=true, +PreferencesTab = defclass(PreferencesTab, ConfigPanel) +PreferencesTab.ATTRS{ intro_text='These are the customizable DFHack system settings.', - select_label='Edit setting', } -function Preferences:init() - self.subviews.show_help_label.visible = false - self.subviews.launch.visible = false - self:addviews{ - widgets.HotkeyLabel{ - frame={b=0, l=0}, - label='Restore defaults', - key='CUSTOM_CTRL_G', - on_activate=self:callback('restore_defaults') +function PreferencesTab:init_main_panel(panel) + panel:addviews{ + widgets.FilteredList{ + frame={t=5}, + view_id='list', + on_select=self:callback('on_select'), + on_double_click=self:callback('on_submit'), + row_height=2, }, IntegerInputDialog{ view_id='input_dlg', @@ -643,9 +705,24 @@ function Preferences:init() } end -function Preferences:onInput(keys) - -- call grandparent's onInput since we don't want ConfigPanel's processing - local handled = Preferences.super.super.onInput(self, keys) +function PreferencesTab:init_footer(panel) + panel:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='Toggle/edit setting', + key='SELECT', + auto_width=true, + on_activate=self:callback('on_submit') + }, + } +end + +function PreferencesTab:onInput(keys) + if self.subviews.input_dlg.visible then + self.subviews.input_dlg:onInput(keys) + return true + end + local handled = PreferencesTab.super.onInput(self, keys) if keys._MOUSE_L then local list = self.subviews.list.list local idx = list:getIdxUnderMouse() @@ -669,24 +746,17 @@ local function make_preference_text(label, value) } end -function Preferences:refresh() +function PreferencesTab:refresh() if self.subviews.input_dlg.visible then return end local choices = {} - for ctx_name,settings in pairs(PREFERENCES) do - local ctx_env = require(ctx_name) - for id,spec in pairs(settings) do - local text = make_preference_text(spec.label, ctx_env[id]) - table.insert(choices, - {text=text, desc=spec.desc, search_key=text[#text], - ctx_env=ctx_env, id=id, spec=spec}) - end + for _, data in ipairs(registry.PREFERENCES_BY_IDX) do + local text = make_preference_text(data.label, data.get_fn()) + table.insert(choices, { + text=text, + search_key=text[#text], + data=data + }) end - for _,spec in ipairs(CPP_PREFERENCES) do - local text = make_preference_text(spec.label, spec.get_fn()) - table.insert(choices, - {text=text, desc=spec.desc, search_key=text[#text], spec=spec}) - end - table.sort(choices, function(a, b) return a.spec.label < b.spec.label end) local list = self.subviews.list local filter = list:getFilter() local selected = list:getSelected() @@ -695,165 +765,40 @@ function Preferences:refresh() list.edit:setFocus(true) end -local function preferences_set_and_save(self, choice, val) - if choice.spec.set_fn then - choice.spec.set_fn(val) - else - choice.ctx_env[choice.id] = val - end - self:do_save() +local function preferences_set_and_save(self, data, val) + common.set_preference(data, val) + common.config:write() self:refresh() end -function Preferences:on_submit() +function PreferencesTab:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end - local cur_val - if choice.spec.get_fn then - cur_val = choice.spec.get_fn() - else - cur_val = choice.ctx_env[choice.id] - end - if choice.spec.type == 'bool' then - preferences_set_and_save(self, choice, not cur_val) - elseif choice.spec.type == 'int' then - self.subviews.input_dlg:show(choice.id or choice.spec.label, choice.spec, cur_val) + local data = choice.data + local cur_val = data.get_fn() + local data_type = type(data.default) + if data_type == 'boolean' then + preferences_set_and_save(self, data, not cur_val) + elseif data_type == 'number' then + self.subviews.input_dlg:show(data.label, data, cur_val) end end -function Preferences:set_val(val) +function PreferencesTab:set_val(val) _,choice = self.subviews.list:getSelected() if not choice or not val then return end - preferences_set_and_save(self, choice, val) -end - -function Preferences:do_save() - local save_fn = function(f) - for ctx_name,settings in pairs(PREFERENCES) do - local ctx_env = require(ctx_name) - for id in pairs(settings) do - f:write((':lua require("%s").%s=%s\n'):format( - ctx_name, id, tostring(ctx_env[id]))) - end - end - for _,spec in ipairs(CPP_PREFERENCES) do - local line = spec.init_fmt:format(spec.get_fn()) - f:write(('%s\n'):format(line)) - end - end - save_file(PREFERENCES_INIT_FILE, save_fn) + preferences_set_and_save(self, choice.data, val) end -function Preferences:restore_defaults() - for ctx_name,settings in pairs(PREFERENCES) do - local ctx_env = require(ctx_name) - for id,spec in pairs(settings) do - ctx_env[id] = spec.default - end +function PreferencesTab:restore_defaults() + for _,data in ipairs(registry.PREFERENCES_BY_IDX) do + common.set_preference(data, data.default) end - for _,spec in ipairs(CPP_PREFERENCES) do - spec.set_fn(spec.default) - end - os.remove(PREFERENCES_INIT_FILE) + common.config:write() self:refresh() dialogs.showMessage('Success', 'Default preferences restored.') end --- --- RepeatAutostart --- - -RepeatAutostart = defclass(RepeatAutostart, ConfigPanel) -RepeatAutostart.ATTRS{ - title='Periodic', - is_enableable=true, - is_configurable=false, - intro_text='Tools that can run periodically to fix bugs or warn you of'.. - ' dangers that are otherwise difficult to detect (like'.. - ' starving caged animals).', -} - -function RepeatAutostart:init() - self.subviews.show_help_label.visible = false - self.subviews.launch.visible = false - local enabled_map = {} - local ok, f = pcall(io.open, REPEATS_FILE) - if ok and f then - for line in f:lines() do - line = line:trim() - if #line == 0 or line:startswith('#') then goto continue end - local service = line:match('^repeat %-%-name ([%S]+)') - if service then - enabled_map[service] = true - end - ::continue:: - end - end - self.enabled_map = enabled_map -end - -function RepeatAutostart:onInput(keys) - -- call grandparent's onInput since we don't want ConfigPanel's processing - local handled = RepeatAutostart.super.super.onInput(self, keys) - if keys._MOUSE_L then - local list = self.subviews.list.list - local idx = list:getIdxUnderMouse() - if idx then - local x = list:getMousePos() - if x <= 2 then - self:on_submit() - end - end - end - return handled -end - -function RepeatAutostart:refresh() - local choices = {} - for _,name in ipairs(REPEATS_LIST) do - local enabled = self.enabled_map[name] - local text = { - {tile=enabled and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT}, - {tile=enabled and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER}, - {tile=enabled and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT}, - ' ', - name, - } - table.insert(choices, - {text=text, desc=REPEATS[name].desc, search_key=name, - name=name, enabled=enabled}) - end - local list = self.subviews.list - local filter = list:getFilter() - local selected = list:getSelected() - list:setChoices(choices) - list:setFilter(filter, selected) - list.edit:setFocus(true) -end - -function RepeatAutostart:on_submit() - _,choice = self.subviews.list:getSelected() - if not choice then return end - self.enabled_map[choice.name] = not choice.enabled - local run_commands = dfhack.isMapLoaded() - - local save_fn = function(f) - for name,enabled in pairs(self.enabled_map) do - if enabled then - local command_str = ('repeat --name %s %s\n'): - format(name, table.concat(REPEATS[name].command, ' ')) - f:write(command_str) - if run_commands then - dfhack.run_command(command_str) -- actually start it up too - end - elseif run_commands then - repeatUtil.cancel(name) - end - end - end - save_file(REPEATS_FILE, save_fn) - self:refresh() -end -- -- ControlPanel @@ -862,9 +807,9 @@ end ControlPanel = defclass(ControlPanel, widgets.Window) ControlPanel.ATTRS { frame_title='DFHack Control Panel', - frame={w=61, h=36}, + frame={w=68, h=44}, resizable=true, - resize_min={h=28}, + resize_min={h=39}, autoarrange_subviews=true, autoarrange_gap=1, } @@ -874,12 +819,10 @@ function ControlPanel:init() widgets.TabBar{ frame={t=0}, labels={ - 'Fort', - 'Maintenance', - 'System', - 'Overlays', - 'Preferences', + 'Enabled', 'Autostart', + 'UI Overlays', + 'Preferences', }, on_select=self:callback('set_page'), get_cur_page=function() return self.subviews.pages:getSelected() end, @@ -888,12 +831,10 @@ function ControlPanel:init() view_id='pages', frame={t=5, l=0, b=0, r=0}, subviews={ - FortServices{}, - RepeatAutostart{}, - SystemServices{}, - Overlays{}, - Preferences{}, - FortServicesAutostart{}, + EnabledTab{}, + AutostartTab{}, + OverlaysTab{}, + PreferencesTab{}, }, }, } @@ -911,6 +852,7 @@ function ControlPanel:set_page(val) self:updateLayout() end + -- -- ControlPanelScreen -- diff --git a/internal/control-panel/common.lua b/internal/control-panel/common.lua new file mode 100644 index 0000000000..a2aa7b86b5 --- /dev/null +++ b/internal/control-panel/common.lua @@ -0,0 +1,192 @@ +--@module = true + +local helpdb = require('helpdb') +local json = require('json') +local migration = reqscript('internal/control-panel/migration') +local persist = require('persist-table') +local registry = reqscript('internal/control-panel/registry') +local repeatUtil = require('repeat-util') +local utils = require('utils') + +local CONFIG_FILE = 'dfhack-config/control-panel.json' + +REPEATS_GLOBAL_KEY = 'control-panel-repeats' + +local function get_config() + local f = json.open(CONFIG_FILE) + local updated = false + -- ensure proper structure + ensure_key(f.data, 'commands') + ensure_key(f.data, 'preferences') + if f.exists then + -- remove unknown or out of date entries from the loaded config + for k in pairs(f.data) do + if k ~= 'commands' and k ~= 'preferences' then + updated = true + f.data[k] = nil + end + end + for name, config_command_data in pairs(f.data.commands) do + local data = registry.COMMANDS_BY_NAME[name] + if not data or config_command_data.version ~= data.version then + updated = true + f.data.commands[name] = nil + end + end + for name, config_pref_data in pairs(f.data.preferences) do + local data = registry.PREFERENCES_BY_NAME[name] + if not data or config_pref_data.version ~= data.version then + updated = true + f.data.preferences[name] = nil + end + end + else + -- migrate any data from old configs + migration.migrate(f.data) + updated = next(f.data.commands) or next(f.data.preferences) + end + if updated then + f:write() + end + return f +end + +config = config or get_config() + +local function unmunge_repeat_name(munged_name) + if munged_name:startswith('control-panel/') then + return munged_name:sub(15) + end +end + +function get_enabled_map() + local enabled_map = {} + local output = dfhack.run_command_silent('enable'):split('\n+') + for _,line in ipairs(output) do + local _,_,command,enabled_str = line:find('%s*(%S+):%s+(%S+)') + if enabled_str then + enabled_map[command] = enabled_str == 'on' + end + end + -- repeat entries override tool names for control-panel + for munged_name in pairs(repeatUtil.repeating) do + local name = unmunge_repeat_name(munged_name) + if name then + enabled_map[name] = true + end + end + return enabled_map +end + +function get_first_word(str) + local word = str:trim():split(' +')[1] + if word:startswith(':') then word = word:sub(2) end + return word +end + +function command_passes_filters(data, target_group, filter_strs) + if data.group ~= target_group then + return false + end + filter_strs = filter_strs or {} + local first_word = get_first_word(data.command) + if dfhack.getHideArmokTools() and helpdb.is_entry(first_word) + and helpdb.get_entry_tags(first_word).armok + then + return false + end + if not utils.search_text(data.command, filter_strs) then + return false + end + return true +end + +function get_description(data) + if data.desc then + return data.desc + end + local first_word = get_first_word(data.command) + return helpdb.is_entry(first_word) and helpdb.get_entry_short_help(first_word) or '' +end + +local function persist_enabled_repeats() + local cp_repeats = {} + for munged_name in pairs(repeatUtil.repeating) do + local name = unmunge_repeat_name(munged_name) + if name then + cp_repeats[name] = true + end + end + persist.GlobalTable[REPEATS_GLOBAL_KEY] = json.encode(cp_repeats) +end + +function apply_command(data, enabled_map, enabled) + enabled_map = enabled_map or {} + if enabled == nil then + enabled = safe_index(config.data.commands, data.command, 'autostart') + enabled = enabled or (enabled == nil and data.default) + if not enabled then return end + end + if data.mode == 'enable' or data.mode == 'system_enable' then + if enabled_map[data.command] == nil then + dfhack.printerr(('tool not enableable: "%s"'):format(data.command)) + return false + else + dfhack.run_command({enabled and 'enable' or 'disable', data.command}) + end + elseif data.mode == 'repeat' then + local munged_name = 'control-panel/' .. data.command + if enabled then + local command_str = ('repeat --name %s %s\n'): + format(munged_name, table.concat(data.params, ' ')) + dfhack.run_command(command_str) + else + repeatUtil.cancel(munged_name) + end + persist_enabled_repeats() + elseif data.mode == 'run' then + if enabled then + dfhack.run_command(data.command) + end + else + dfhack.printerr(('unhandled command: "%s"'):format(data.command)) + return false + end + return true +end + +function set_preference(data, in_value) + local expected_type = type(data.default) + local value = in_value + if expected_type == 'boolean' and type(value) ~= 'boolean' then + value = argparse.boolean(value) + end + local actual_type = type(value) + if actual_type ~= expected_type then + qerror(('"%s" has an unexpected value type: got: %s; expected: %s'):format( + in_value, actual_type, expected_type)) + end + if data.min and data.min > value then + qerror(('value too small: got: %s; minimum: %s'):format(value, data.min)) + end + data.set_fn(value) + if data.default ~= value then + config.data.preferences[data.name] = { + val=value, + version=data.version, + } + else + config.data.preferences[data.name] = nil + end +end + +function set_autostart(data, enabled) + if enabled ~= not not data.default then + config.data.commands[data.command] = { + autostart=enabled, + version=data.version, + } + else + config.data.commands[data.command] = nil + end +end diff --git a/internal/control-panel/migration.lua b/internal/control-panel/migration.lua new file mode 100644 index 0000000000..cb0a639a8f --- /dev/null +++ b/internal/control-panel/migration.lua @@ -0,0 +1,131 @@ +-- migrate configuration from 50.11-r4 and prior to new format +--@module = true + +-- read old files, add converted data to config_data, overwrite old files with +-- a message that says they are deprecated and can be deleted with the proper +-- procedure. we can't delete them outright since steam may just restore them due to +-- Steam Cloud. We *could* delete them, though, if we know that we've been started +-- from Steam as DFHack and not as DF + +local argparse = require('argparse') +local registry = reqscript('internal/control-panel/registry') + +-- init files +local SYSTEM_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-system.init' +local AUTOSTART_FILE = 'dfhack-config/init/onMapLoad.control-panel-new-fort.init' +local REPEATS_FILE = 'dfhack-config/init/onMapLoad.control-panel-repeats.init' +local PREFERENCES_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-preferences.init' + +local function save_tombstone_file(path) + local ok, f = pcall(io.open, path, 'w') + if not ok or not f then + dialogs.showMessage('Error', + ('Cannot open file for writing: "%s"'):format(path)) + return + end + f:write('# This file was once used by gui/control-panel\n') + f:write('# If you are on Steam, you can delete this file manually\n') + f:write('# by starting DFHack in the Steam client, then deleting\n') + f:write('# this file while DF is running. Otherwise Steam Cloud will\n') + f:write('# restore the file when you next run DFHack.\n') + f:write('#\n') + f:write('# If you\'re not on Steam, you can delete this file at any time.\n') + f:close() +end + +local function add_autostart(config_data, name) + if not registry.COMMANDS_BY_NAME[name].default then + config_data.commands[name] = {autostart=true} + end +end + +local function add_preference(config_data, name, val) + local data = registry.PREFERENCES_BY_NAME[name] + if type(data.default) == 'boolean' then + ok, val = pcall(argparse.boolean, val) + if not ok then return end + elseif type(data.default) == 'number' then + val = tonumber(val) + if not val then return end + end + if data.default ~= val then + config_data.preferences[name] = {val=val} + end +end + +local function parse_lines(fname, line_fn) + local ok, f = pcall(io.open, fname) + if not ok or not f then return end + for line in f:lines() do + line = line:trim() + if #line > 0 and not line:startswith('#') then + line_fn(line) + end + end +end + +local function migrate_system(config_data) + parse_lines(SYSTEM_INIT_FILE, function(line) + local service = line:match('^enable ([%S]+)$') + if not service then return end + local data = registry.COMMANDS_BY_NAME[service] + if data and (data.mode == 'system_enable' or data.command == 'work-now') then + add_autostart(config_data, service) + end + end) + save_tombstone_file(SYSTEM_INIT_FILE) +end + +local function migrate_autostart(config_data) + parse_lines(AUTOSTART_FILE, function(line) + local service = line:match('^on%-new%-fortress enable ([%S]+)$') + or line:match('^on%-new%-fortress (.+)') + if not service then return end + local data = registry.COMMANDS_BY_NAME[service] + if data and (data.mode == 'enable' or data.mode == 'run') then + add_autostart(config_data, service) + end + end) + save_tombstone_file(AUTOSTART_FILE) +end + +local REPEAT_MAP = { + autoMilkCreature='automilk', + autoShearCreature='autoshear', + ['dead-units-burrow']='fix/dead-units', + ['empty-wheelbarrows']='fix/empty-wheelbarrows', + ['general-strike']='fix/general-strike', + ['stuck-instruments']='fix/stuck-instruments', +} + +local function migrate_repeats(config_data) + parse_lines(REPEATS_FILE, function(line) + local service = line:match('^repeat %-%-name ([%S]+)') + if not service then return end + service = REPEAT_MAP[service] or service + local data = registry.COMMANDS_BY_NAME[service] + if data and data.mode == 'repeat' then + add_autostart(config_data, service) + end + end) + save_tombstone_file(REPEATS_FILE) +end + +local function migrate_preferences(config_data) + parse_lines(PREFERENCES_INIT_FILE, function(line) + local name, val = line:match('^:lua .+%.([^=]+)=(.+)') + if not name or not val then return end + local data = registry.PREFERENCES_BY_NAME[name] + if data then + add_preference(config_data, name, val) + end + end) + save_tombstone_file(PREFERENCES_INIT_FILE) +end + +function migrate(config_data) + migrate_system(config_data) + migrate_autostart(config_data) + migrate_repeats(config_data) + migrate_preferences(config_data) +end diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua new file mode 100644 index 0000000000..0e458b0fd1 --- /dev/null +++ b/internal/control-panel/registry.lua @@ -0,0 +1,172 @@ +--@module = true + +local gui = require('gui') +local widgets = require('gui.widgets') +local utils = require('utils') + +-- please keep in alphabetical order per group +-- add a 'version' attribute if we want to reset existing configs for a command to the default +COMMANDS_BY_IDX = { + -- automation tools + {command='autobutcher', group='automation', mode='enable'}, + {command='autobutcher target 10 10 14 2 BIRD_GOOSE', group='automation', mode='run', + desc='Enable if you usually want to raise geese.'}, + {command='autobutcher target 10 10 14 2 BIRD_TURKEY', group='automation', mode='run', + desc='Enable if you usually want to raise turkeys.'}, + {command='autobutcher target 10 10 14 2 BIRD_CHICKEN', group='automation', mode='run', + desc='Enable if you usually want to raise chickens.'}, + {command='autochop', group='automation', mode='enable'}, + {command='autoclothing', group='automation', mode='enable'}, + {command='autofarm', group='automation', mode='enable'}, + {command='autofarm threshold 150 grass_tail_pig', group='automation', mode='run', + desc='Enable if you usually farm pig tails for the clothing industry.'}, + {command='autofish', group='automation', mode='enable'}, + --{command='autolabor', group='automation', mode='enable'}, -- hide until it works better + {command='automilk', group='automation', mode='repeat', + desc='Automatically milk creatures that are ready for milking.', + params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', '"{\\"job\\":\\"MilkCreature\\",\\"item_conditions\\":[{\\"condition\\":\\"AtLeast\\",\\"value\\":2,\\"flags\\":[\\"empty\\"],\\"item_type\\":\\"BUCKET\\"}]}"', ']'}}, + {command='autonestbox', group='automation', mode='enable'}, + {command='autoshear', group='automation', mode='repeat', + desc='Automatically shear creatures that are ready for shearing.', + params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', 'ShearCreature', ']'}}, + {command='autoslab', group='automation', mode='enable'}, + {command='ban-cooking all', group='automation', mode='run'}, + {command='buildingplan set boulders false', group='automation', mode='run', + desc='Enable if you usually don\'t want to use boulders for construction.'}, + {command='buildingplan set logs false', group='automation', mode='run', + desc='Enable if you usually don\'t want to use logs for construction.'}, + {command='cleanowned', group='automation', mode='repeat', + desc='Encourage dwarves to drop tattered clothing and grab new ones.', + params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, + {command='nestboxes', group='automation', mode='enable'}, + {command='orders-sort', group='automation', mode='repeat', + desc='Sort manager orders by repeat frequency so one-time orders can be completed.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, + {command='prioritize', group='automation', mode='enable'}, + {command='seedwatch', group='automation', mode='enable'}, + {command='suspendmanager', group='automation', mode='enable'}, + {command='tailor', group='automation', mode='enable'}, + {command='work-now', group='automation', mode='enable'}, + + -- bugfix tools + {command='fix/blood-del', group='bugfix', mode='run', default=true}, + {command='fix/dead-units', group='bugfix', mode='repeat', default=true, + desc='Fix units still being assigned to burrows after death.', + params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, + {command='fix/empty-wheelbarrows', group='bugfix', mode='repeat', default=true, + desc='Make abandoned full wheelbarrows usable again.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, + {command='fix/general-strike', group='bugfix', mode='repeat', default=true, + desc='Prevent dwarves from getting stuck and refusing to work.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/general-strike', '-q', ']'}}, + {command='fix/protect-nicks', group='bugfix', mode='enable', default=true}, + {command='fix/stuck-instruments', group='bugfix', mode='repeat', default=true, + desc='Fix activity references on stuck instruments to make them usable again.', + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, + {command='preserve-tombs', group='bugfix', mode='enable', default=true}, + + -- gameplay tools + {command='combine', group='gameplay', mode='repeat', + desc='Combine partial stacks in stockpiles into full stacks.', + params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, + {command='drain-aquifer --top 2', group='gameplay', mode='run', + desc='Ensure that your maps have no more than 2 layers of aquifer.'}, + {command='dwarfvet', group='gameplay', mode='enable'}, + {command='emigration', group='gameplay', mode='enable'}, + {command='fastdwarf', group='gameplay', mode='enable'}, + {command='hermit', group='gameplay', mode='enable'}, + {command='hide-tutorials', group='gameplay', mode='system_enable'}, + {command='light-aquifers-only', group='gameplay', mode='run'}, + {command='misery', group='gameplay', mode='enable'}, + {command='orders-reevaluate', group='gameplay', mode='repeat', + desc='Invalidates all work orders once a month, allowing conditions to be rechecked.', + params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, + {command='starvingdead', group='gameplay', mode='enable'}, + {command='warn-starving', group='gameplay', mode='repeat', + desc='Show a warning dialog when units are starving or dehydrated.', + params={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}}, + {command='warn-stranded', group='gameplay', mode='repeat', + desc='Show a warning dialog when units are stranded from all others.', + params={'--time', '2', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, +} + +COMMANDS_BY_NAME = {} +for _,data in ipairs(COMMANDS_BY_IDX) do + COMMANDS_BY_NAME[data.command] = data +end + +-- keep in desired display order +PREFERENCES_BY_IDX = { + { + name='HIDE_ARMOK_TOOLS', + label='Mortal mode: hide "armok" tools', + desc='Don\'t show tools that give you god-like powers wherever DFHack tools are listed.', + default=false, + get_fn=function() return dfhack.HIDE_ARMOK_TOOLS end, + set_fn=function(val) dfhack.HIDE_ARMOK_TOOLS = val end, + }, + { + name='FILTER_FULL_TEXT', + label='DFHack searches full text', + desc='When searching, whether to match anywhere in the text (true) or just at the start of words (false).', + default=false, + get_fn=function() return utils.FILTER_FULL_TEXT end, + set_fn=function(val) utils.FILTER_FULL_TEXT = val end, + }, + { + name='HIDE_CONSOLE_ON_STARTUP', + label='Hide console on startup (MS Windows only)', + desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.', + default=true, + get_fn=function() return dfhack.HIDE_CONSOLE_ON_STARTUP end, + set_fn=function(val) dfhack.HIDE_CONSOLE_ON_STARTUP = val end, + }, + { + name='DEFAULT_INITIAL_PAUSE', + label='DFHack tools autopause game', + desc='Always pause the game when a DFHack tool window is shown (you can still unpause afterwards).', + default=true, + get_fn=function() return gui.DEFAULT_INITIAL_PAUSE end, + set_fn=function(val) gui.DEFAULT_INITIAL_PAUSE = val end, + }, + { + name='INTERCEPT_HANDLED_HOTKEYS', + label='Intercept handled hotkeys', + desc='Prevent key events handled by DFHack windows from also affecting the vanilla widgets.', + default=true, + get_fn=dfhack.internal.getSuppressDuplicateKeyboardEvents, + set_fn=dfhack.internal.setSuppressDuplicateKeyboardEvents, + }, + { + name='DOUBLE_CLICK_MS', + label='Mouse double click speed (ms)', + desc='How long to wait for the second click of a double click, in ms.', + default=500, + min=50, + get_fn=function() return widgets.DOUBLE_CLICK_MS end, + set_fn=function(val) widgets.DOUBLE_CLICK_MS = val end, + }, + { + name='SCROLL_DELAY_MS', + label='Mouse scroll repeat delay (ms)', + desc='The delay between events when holding the mouse button down on a scrollbar, in ms.', + default=20, + min=5, + get_fn=function() return widgets.SCROLL_DELAY_MS end, + set_fn=function(val) widgets.SCROLL_DELAY_MS = val end, + }, + { + name='SCROLL_INITIAL_DELAY_MS', + label='Mouse initial scroll repeat delay (ms)', + desc='The delay before scrolling quickly when holding the mouse button down on a scrollbar, in ms.', + default=300, + min=5, + get_fn=function() return widgets.SCROLL_INITIAL_DELAY_MS end, + set_fn=function(val) widgets.SCROLL_INITIAL_DELAY_MS = val end, + }, +} + +PREFERENCES_BY_NAME = {} +for _,data in ipairs(PREFERENCES_BY_IDX) do + PREFERENCES_BY_NAME[data.name] = data +end