From 8dc50e159a0d4959510dd4033d4f3f278f3f2219 Mon Sep 17 00:00:00 2001 From: unboundlopez <47876628+unboundlopez@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:14:19 -0600 Subject: [PATCH 1/6] Document external TTS usage --- flavor-text.lua | 176 ++++++++++++++++++++++++++++++++++++++++++++++++ flavor-text.rst | 27 ++++++++ 2 files changed, 203 insertions(+) create mode 100644 flavor-text.lua create mode 100644 flavor-text.rst diff --git a/flavor-text.lua b/flavor-text.lua new file mode 100644 index 000000000..9d171e9b7 --- /dev/null +++ b/flavor-text.lua @@ -0,0 +1,176 @@ +-- Writes the currently viewed unit or item flavor text to a file. +-- +-- Usage: +-- flavor-text + +local folder = 'flavor text' +local filename = 'read flavor.txt' +local filepath = folder .. '/' .. filename + +local function clear_output_file() + local ok, err = pcall(dfhack.filesystem.mkdir_recursive, folder) + if not ok then + qerror(('Failed to create folder "%s": %s'):format(folder, err)) + end + local file, open_err = io.open(filepath, 'w') + if not file then + qerror(('Failed to open file "%s" for writing: %s'):format(filepath, open_err)) + end + file:write('') + file:close() +end + +local function reformat(str) + local cleaned = str:gsub('%[B%]', '') + :gsub('%[P%]', '') + :gsub('%[R%]', '') + :gsub('%[C:%d+:%d+:%d+%]', '') + :gsub('%s+', ' ') + :gsub('^%s+', '') + :gsub('%s+$', '') + return cleaned +end + +local function collect_lines(entries) + local lines = {} + for _, entry in ipairs(entries) do + if entry.value ~= '' then + local cleaned = reformat(dfhack.df2utf(entry.value)) + if cleaned ~= '' then + table.insert(lines, cleaned) + end + end + end + return lines +end + +local function get_health_text(view_sheets) + if #view_sheets.unit_health_raw_str == 0 then + return nil + end + local lines = collect_lines(view_sheets.unit_health_raw_str) + if #lines == 0 then + return nil + end + return table.concat(lines, '\n') +end + +local function get_personality_text(view_sheets) + if #view_sheets.personality_raw_str == 0 then + return nil + end + local lines = collect_lines(view_sheets.personality_raw_str) + if #lines == 0 then + return nil + end + return table.concat(lines, '\n') +end + +local UNIT_SHEET_SUBTAB = { + HEALTH = 2, + PERSONALITY = 10, +} + +local HEALTH_ACTIVE_TAB = { + STATUS = 0, + WOUNDS = 1, + TREATMENT = 2, + HISTORY = 3, + DESCRIPTION = 4, +} + +local PERSONALITY_ACTIVE_TAB = { + TRAITS = 0, + VALUES = 1, + PREFERENCES = 2, + NEEDS = 3, +} + +local function get_unit_flavor_text(view_sheets) + local unit = df.unit.find(view_sheets.active_id) + if not unit then + qerror('Unable to resolve the active unit.') + end + + if view_sheets.active_sub_tab == UNIT_SHEET_SUBTAB.HEALTH then + local health_text = get_health_text(view_sheets) + if health_text then + return unit, 'Health', health_text + end + clear_output_file() + qerror('No text found on the Health tab.') + end + + if view_sheets.active_sub_tab == UNIT_SHEET_SUBTAB.PERSONALITY + and (view_sheets.personality_active_tab == PERSONALITY_ACTIVE_TAB.TRAITS + or view_sheets.personality_active_tab == PERSONALITY_ACTIVE_TAB.VALUES + or view_sheets.personality_active_tab == PERSONALITY_ACTIVE_TAB.PREFERENCES + or view_sheets.personality_active_tab == PERSONALITY_ACTIVE_TAB.NEEDS) + then + local text = get_personality_text(view_sheets) + if text then + return unit, 'Personality', text + end + clear_output_file() + qerror('No text found on the Personality subtab (Traits/Values/Preferences/Needs).') + end + + clear_output_file() + qerror('Open Health, Personality or an item window before running this script.') +end + +local function get_item_flavor_text(view_sheets) + local item = dfhack.gui.getSelectedItem(true) + if not item then + qerror('Select an item or open an item view sheet before running this script.') + end + + local description = view_sheets.raw_description or '' + if description == '' then + qerror('No item description text found on the item view sheet.') + end + + return item, 'Item', reformat(dfhack.df2utf(description)) +end + +local view_sheets = df.global.game.main_interface.view_sheets +if not view_sheets.open then + clear_output_file() + qerror('Open a unit or item view sheet before running this script.') +end + +local screen = dfhack.gui.getDFViewscreen() +local is_unit_sheet = dfhack.gui.matchFocusString('dwarfmode/ViewSheets/UNIT', screen) +local is_item_sheet = dfhack.gui.matchFocusString('dwarfmode/ViewSheets/ITEM', screen) + +local subject, flavor_type, text +if is_unit_sheet then + subject, flavor_type, text = get_unit_flavor_text(view_sheets) +elseif is_item_sheet then + subject, flavor_type, text = get_item_flavor_text(view_sheets) +else + clear_output_file() + qerror('Open a unit or item view sheet before running this script.') +end + +local ok, err = pcall(dfhack.filesystem.mkdir_recursive, folder) +if not ok then + qerror(('Failed to create folder "%s": %s'):format(folder, err)) +end + +local file, open_err = io.open(filepath, 'w') +if not file then + qerror(('Failed to open file "%s" for writing: %s'):format(filepath, open_err)) +end + +file:write(text) +file:close() + +local name +if is_unit_sheet then + name = dfhack.df2console(dfhack.units.getReadableName(subject)) +else + name = dfhack.df2console(dfhack.items.getDescription(subject, 0, true)) +end + +print(('Wrote %s flavor text for %s to "%s".'):format(flavor_type, name, filepath)) diff --git a/flavor-text.rst b/flavor-text.rst new file mode 100644 index 000000000..9a6a5a4c0 --- /dev/null +++ b/flavor-text.rst @@ -0,0 +1,27 @@ +flavor-text +=========== + +Overview +-------- +The ``flavor-text`` script writes the currently viewed unit or item flavor text to +``flavor text/read flavor.txt``. + +Usage +----- +Run the script from DFHack: + +:: + + flavor-text + +Notes +----- +- The file is overwritten each time the script runs. +- If the wrong window is open, the script clears the output file. +- Supported unit tabs: + - Health (Status/Wounds/Treatment/History/Description) + - Personality (Traits/Values/Preferences/Needs) +- Supported item window: item view sheets. +- You must use your own text-to-speech (TTS) program to read the output. + On Windows 11, Voice Attack works well, and a profile with the launch command + can be included for others to use. From b2c63f5fb4f46110f927174de372ba9ee172c617 Mon Sep 17 00:00:00 2001 From: unboundlopez <47876628+unboundlopez@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:25:08 -0600 Subject: [PATCH 2/6] Delete flavor-text.lua --- flavor-text.lua | 176 ------------------------------------------------ 1 file changed, 176 deletions(-) delete mode 100644 flavor-text.lua diff --git a/flavor-text.lua b/flavor-text.lua deleted file mode 100644 index 9d171e9b7..000000000 --- a/flavor-text.lua +++ /dev/null @@ -1,176 +0,0 @@ --- Writes the currently viewed unit or item flavor text to a file. --- --- Usage: --- flavor-text - -local folder = 'flavor text' -local filename = 'read flavor.txt' -local filepath = folder .. '/' .. filename - -local function clear_output_file() - local ok, err = pcall(dfhack.filesystem.mkdir_recursive, folder) - if not ok then - qerror(('Failed to create folder "%s": %s'):format(folder, err)) - end - local file, open_err = io.open(filepath, 'w') - if not file then - qerror(('Failed to open file "%s" for writing: %s'):format(filepath, open_err)) - end - file:write('') - file:close() -end - -local function reformat(str) - local cleaned = str:gsub('%[B%]', '') - :gsub('%[P%]', '') - :gsub('%[R%]', '') - :gsub('%[C:%d+:%d+:%d+%]', '') - :gsub('%s+', ' ') - :gsub('^%s+', '') - :gsub('%s+$', '') - return cleaned -end - -local function collect_lines(entries) - local lines = {} - for _, entry in ipairs(entries) do - if entry.value ~= '' then - local cleaned = reformat(dfhack.df2utf(entry.value)) - if cleaned ~= '' then - table.insert(lines, cleaned) - end - end - end - return lines -end - -local function get_health_text(view_sheets) - if #view_sheets.unit_health_raw_str == 0 then - return nil - end - local lines = collect_lines(view_sheets.unit_health_raw_str) - if #lines == 0 then - return nil - end - return table.concat(lines, '\n') -end - -local function get_personality_text(view_sheets) - if #view_sheets.personality_raw_str == 0 then - return nil - end - local lines = collect_lines(view_sheets.personality_raw_str) - if #lines == 0 then - return nil - end - return table.concat(lines, '\n') -end - -local UNIT_SHEET_SUBTAB = { - HEALTH = 2, - PERSONALITY = 10, -} - -local HEALTH_ACTIVE_TAB = { - STATUS = 0, - WOUNDS = 1, - TREATMENT = 2, - HISTORY = 3, - DESCRIPTION = 4, -} - -local PERSONALITY_ACTIVE_TAB = { - TRAITS = 0, - VALUES = 1, - PREFERENCES = 2, - NEEDS = 3, -} - -local function get_unit_flavor_text(view_sheets) - local unit = df.unit.find(view_sheets.active_id) - if not unit then - qerror('Unable to resolve the active unit.') - end - - if view_sheets.active_sub_tab == UNIT_SHEET_SUBTAB.HEALTH then - local health_text = get_health_text(view_sheets) - if health_text then - return unit, 'Health', health_text - end - clear_output_file() - qerror('No text found on the Health tab.') - end - - if view_sheets.active_sub_tab == UNIT_SHEET_SUBTAB.PERSONALITY - and (view_sheets.personality_active_tab == PERSONALITY_ACTIVE_TAB.TRAITS - or view_sheets.personality_active_tab == PERSONALITY_ACTIVE_TAB.VALUES - or view_sheets.personality_active_tab == PERSONALITY_ACTIVE_TAB.PREFERENCES - or view_sheets.personality_active_tab == PERSONALITY_ACTIVE_TAB.NEEDS) - then - local text = get_personality_text(view_sheets) - if text then - return unit, 'Personality', text - end - clear_output_file() - qerror('No text found on the Personality subtab (Traits/Values/Preferences/Needs).') - end - - clear_output_file() - qerror('Open Health, Personality or an item window before running this script.') -end - -local function get_item_flavor_text(view_sheets) - local item = dfhack.gui.getSelectedItem(true) - if not item then - qerror('Select an item or open an item view sheet before running this script.') - end - - local description = view_sheets.raw_description or '' - if description == '' then - qerror('No item description text found on the item view sheet.') - end - - return item, 'Item', reformat(dfhack.df2utf(description)) -end - -local view_sheets = df.global.game.main_interface.view_sheets -if not view_sheets.open then - clear_output_file() - qerror('Open a unit or item view sheet before running this script.') -end - -local screen = dfhack.gui.getDFViewscreen() -local is_unit_sheet = dfhack.gui.matchFocusString('dwarfmode/ViewSheets/UNIT', screen) -local is_item_sheet = dfhack.gui.matchFocusString('dwarfmode/ViewSheets/ITEM', screen) - -local subject, flavor_type, text -if is_unit_sheet then - subject, flavor_type, text = get_unit_flavor_text(view_sheets) -elseif is_item_sheet then - subject, flavor_type, text = get_item_flavor_text(view_sheets) -else - clear_output_file() - qerror('Open a unit or item view sheet before running this script.') -end - -local ok, err = pcall(dfhack.filesystem.mkdir_recursive, folder) -if not ok then - qerror(('Failed to create folder "%s": %s'):format(folder, err)) -end - -local file, open_err = io.open(filepath, 'w') -if not file then - qerror(('Failed to open file "%s" for writing: %s'):format(filepath, open_err)) -end - -file:write(text) -file:close() - -local name -if is_unit_sheet then - name = dfhack.df2console(dfhack.units.getReadableName(subject)) -else - name = dfhack.df2console(dfhack.items.getDescription(subject, 0, true)) -end - -print(('Wrote %s flavor text for %s to "%s".'):format(flavor_type, name, filepath)) From 178cfe15e16dc9baf1a5591bdca5b6d9590931b2 Mon Sep 17 00:00:00 2001 From: unboundlopez <47876628+unboundlopez@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:25:24 -0600 Subject: [PATCH 3/6] Delete flavor-text.rst --- flavor-text.rst | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 flavor-text.rst diff --git a/flavor-text.rst b/flavor-text.rst deleted file mode 100644 index 9a6a5a4c0..000000000 --- a/flavor-text.rst +++ /dev/null @@ -1,27 +0,0 @@ -flavor-text -=========== - -Overview --------- -The ``flavor-text`` script writes the currently viewed unit or item flavor text to -``flavor text/read flavor.txt``. - -Usage ------ -Run the script from DFHack: - -:: - - flavor-text - -Notes ------ -- The file is overwritten each time the script runs. -- If the wrong window is open, the script clears the output file. -- Supported unit tabs: - - Health (Status/Wounds/Treatment/History/Description) - - Personality (Traits/Values/Preferences/Needs) -- Supported item window: item view sheets. -- You must use your own text-to-speech (TTS) program to read the output. - On Windows 11, Voice Attack works well, and a profile with the launch command - can be included for others to use. From be482d53656d5305a939599422058df7822066ba Mon Sep 17 00:00:00 2001 From: unboundlopez <47876628+unboundlopez@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:17:40 -0600 Subject: [PATCH 4/6] Add files via upload --- lever-interface.lua | 228 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 lever-interface.lua diff --git a/lever-interface.lua b/lever-interface.lua new file mode 100644 index 000000000..a8d8bcaa0 --- /dev/null +++ b/lever-interface.lua @@ -0,0 +1,228 @@ +-- List and pull levers + +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local utils = require('utils') +local widgets = require('gui.widgets') + +local lever_script = reqscript('lever') + +local REFRESH_MS = 1000 + +local function get_levers() + local levers = {} + for _, building in ipairs(df.global.world.buildings.other.TRAP) do + if building.trap_type == df.trap_type.Lever then + table.insert(levers, building) + end + end + return levers +end + +local function get_lever_label(lever) + local status = (lever.state == 1) and 'Pulled' or 'Not Pulled' + local name = utils.getBuildingName(lever) + local queued = 0 + for _, job in ipairs(lever.jobs) do + if job.job_type == df.job_type.PullLever then + queued = queued + 1 + end + end + local queued_text = queued > 0 and (' (queued: %d)'):format(queued) or '' + return ('[%s] %s (#%d)%s'):format(status, name, lever.id, queued_text) +end + +local function get_queued_count(levers) + local queued = 0 + for _, lever in ipairs(levers) do + for _, job in ipairs(lever.jobs) do + if job.job_type == df.job_type.PullLever then + queued = queued + 1 + end + end + end + return queued +end + +LeverWindow = defclass(LeverWindow, widgets.Window) +LeverWindow.ATTRS{ + frame_title = 'Lever Tasks', + frame = {w=60, h=18, r=2}, +} + +function LeverWindow:init() + local _, screen_height = dfhack.screen.getWindowSize() + if screen_height then + self.frame.t = math.max(0, math.floor((screen_height - self.frame.h) / 2)) + end + self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS + self.filter_text = '' + self:addviews{ + widgets.EditField{ + view_id='search', + frame={t=0, l=0, r=0}, + label_text='Search: ', + on_change=self:callback('set_filter'), + }, + widgets.List{ + view_id='lever_list', + frame={t=1, l=0, r=0, b=4}, + on_submit=self:callback('queue_pull'), + on_select=self:callback('focus_lever'), + }, + widgets.Label{ + view_id='empty_message', + frame={t=1, l=0, r=0}, + text='No levers found.', + visible=false, + }, + widgets.HotkeyLabel{ + frame={b=3, l=0}, + label='Pull selected lever', + key='CUSTOM_P', + on_activate=self:callback('queue_pull'), + }, + widgets.HotkeyLabel{ + frame={b=2, l=0}, + label='Remove queued pulls', + key='CUSTOM_X', + on_activate=self:callback('remove_queued_pulls'), + }, + widgets.Label{ + view_id='queued_count', + frame={b=3, r=0}, + text='Queued pulls: 0', + auto_width=true, + }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + label='Refresh list', + key='CUSTOM_R', + on_activate=self:callback('refresh_list'), + }, + } + + self:refresh_list() +end + +function LeverWindow:set_filter(text) + self.filter_text = text or '' + self:refresh_list() +end + +function LeverWindow:refresh_list() + local list = self.subviews.lever_list + local selected_id + if list then + local _, selected = list:getSelected() + if selected and selected.data then + selected_id = selected.data.id + end + end + + local choices = {} + local levers = get_levers() + table.sort(levers, function(a, b) + if a.state == b.state then + return a.id < b.id + end + return a.state > b.state + end) + local filter = (self.filter_text or ''):lower() + local filtered_levers = {} + if filter == '' then + filtered_levers = levers + else + for _, lever in ipairs(levers) do + local name = utils.getBuildingName(lever) + if name:lower():find(filter, 1, true) then + table.insert(filtered_levers, lever) + end + end + end + local selected_idx = 1 + for idx, lever in ipairs(filtered_levers) do + table.insert(choices, {text=get_lever_label(lever), data=lever}) + if selected_id and lever.id == selected_id then + selected_idx = idx + end + end + list:setChoices(choices, selected_idx) + self.subviews.empty_message.visible = #choices == 0 + self.subviews.queued_count:setText(('Queued pulls: %d'):format(get_queued_count(levers))) +end + +function LeverWindow:queue_pull() + local _, choice = self.subviews.lever_list:getSelected() + if not choice then + return + end + lever_script.leverPullJob(choice.data, false) + self:refresh_list() +end + +function LeverWindow:remove_queued_pulls() + local _, choice = self.subviews.lever_list:getSelected() + if not choice then + return + end + local jobs = {} + for _, job in ipairs(choice.data.jobs) do + if job.job_type == df.job_type.PullLever then + table.insert(jobs, job) + end + end + for _, job in ipairs(jobs) do + dfhack.job.removeJob(job) + end + self:refresh_list() +end + +function LeverWindow:onRenderFrame(dc, rect) + LeverWindow.super.onRenderFrame(self, dc, rect) + + local list = self.subviews.lever_list + local hover_idx = list:getIdxUnderMouse() + if hover_idx and hover_idx ~= self.hover_index then + self.hover_index = hover_idx + list:setSelected(hover_idx) + local _, choice = list:getSelected() + if choice then + self:focus_lever(nil, choice) + end + end +end + +function LeverWindow:onRenderBody() + if dfhack.getTickCount() >= self.next_refresh_ms then + self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS + self:refresh_list() + end +end + +function LeverWindow:focus_lever(_, choice) + if not choice then + return + end + local lever = choice.data + local pos = {x=lever.centerx, y=lever.centery, z=lever.z} + dfhack.gui.revealInDwarfmodeMap(pos, true, true) + guidm.setCursorPos(pos) +end + +LeverScreen = defclass(LeverScreen, gui.ZScreen) +LeverScreen.ATTRS{focus_path='lever'} + +function LeverScreen:init() + self:addviews{LeverWindow{}} +end + +function LeverScreen:onDismiss() + view = nil +end + +if not dfhack.isMapLoaded() then + qerror('gui/lever requires a map to be loaded') +end + +view = view and view:raise() or LeverScreen{}:show() From 8e4406d8fa9bbbb6cffc98373846803d08c69347 Mon Sep 17 00:00:00 2001 From: unboundlopez <47876628+unboundlopez@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:17:52 -0600 Subject: [PATCH 5/6] Add files via upload --- docs/lever-interface.rst | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/lever-interface.rst diff --git a/docs/lever-interface.rst b/docs/lever-interface.rst new file mode 100644 index 000000000..6de88cb8b --- /dev/null +++ b/docs/lever-interface.rst @@ -0,0 +1,32 @@ +Lever Interface +=============== + +Overview +-------- +The lever interface provides a consolidated list of all levers in the current map +and lets you queue or remove pull tasks. The list is kept up to date automatically +so queued, completed, and cancelled pulls are reflected without manual refreshes. + +Main features +------------- +- Lists all levers with a status prefix (``[Pulled]`` or ``[Not Pulled]``). +- Shows queued pull counts per lever and a global queued pull total. +- Allows queuing a pull task for the selected lever. +- Allows removing queued pull tasks from the selected lever. +- Supports hover focus to pan the map to the lever without clicking. +- Supports search filtering by lever name. + +Using the interface +------------------- +- **Search**: Type in the search field to filter levers by name. Filtering is + case-insensitive and matches substrings. +- **Hover**: Move the mouse over a lever entry to pan and highlight the lever. +- **Click**: Click a lever entry (or press Enter) to queue a pull task. +- **Remove queued pulls**: Use the remove hotkey to clear queued pull jobs for + the selected lever. + +Hotkeys +------- +- ``P``: Queue a pull task for the selected lever. +- ``X``: Remove queued pull tasks from the selected lever. +- ``R``: Refresh the list. From 3b5ee59ecd1e3d3df8db0401699551eaf2051d96 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:40:08 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/lever-interface.rst | 64 +++--- lever-interface.lua | 456 +++++++++++++++++++-------------------- 2 files changed, 260 insertions(+), 260 deletions(-) diff --git a/docs/lever-interface.rst b/docs/lever-interface.rst index 6de88cb8b..bbfbeecd2 100644 --- a/docs/lever-interface.rst +++ b/docs/lever-interface.rst @@ -1,32 +1,32 @@ -Lever Interface -=============== - -Overview --------- -The lever interface provides a consolidated list of all levers in the current map -and lets you queue or remove pull tasks. The list is kept up to date automatically -so queued, completed, and cancelled pulls are reflected without manual refreshes. - -Main features -------------- -- Lists all levers with a status prefix (``[Pulled]`` or ``[Not Pulled]``). -- Shows queued pull counts per lever and a global queued pull total. -- Allows queuing a pull task for the selected lever. -- Allows removing queued pull tasks from the selected lever. -- Supports hover focus to pan the map to the lever without clicking. -- Supports search filtering by lever name. - -Using the interface -------------------- -- **Search**: Type in the search field to filter levers by name. Filtering is - case-insensitive and matches substrings. -- **Hover**: Move the mouse over a lever entry to pan and highlight the lever. -- **Click**: Click a lever entry (or press Enter) to queue a pull task. -- **Remove queued pulls**: Use the remove hotkey to clear queued pull jobs for - the selected lever. - -Hotkeys -------- -- ``P``: Queue a pull task for the selected lever. -- ``X``: Remove queued pull tasks from the selected lever. -- ``R``: Refresh the list. +Lever Interface +=============== + +Overview +-------- +The lever interface provides a consolidated list of all levers in the current map +and lets you queue or remove pull tasks. The list is kept up to date automatically +so queued, completed, and cancelled pulls are reflected without manual refreshes. + +Main features +------------- +- Lists all levers with a status prefix (``[Pulled]`` or ``[Not Pulled]``). +- Shows queued pull counts per lever and a global queued pull total. +- Allows queuing a pull task for the selected lever. +- Allows removing queued pull tasks from the selected lever. +- Supports hover focus to pan the map to the lever without clicking. +- Supports search filtering by lever name. + +Using the interface +------------------- +- **Search**: Type in the search field to filter levers by name. Filtering is + case-insensitive and matches substrings. +- **Hover**: Move the mouse over a lever entry to pan and highlight the lever. +- **Click**: Click a lever entry (or press Enter) to queue a pull task. +- **Remove queued pulls**: Use the remove hotkey to clear queued pull jobs for + the selected lever. + +Hotkeys +------- +- ``P``: Queue a pull task for the selected lever. +- ``X``: Remove queued pull tasks from the selected lever. +- ``R``: Refresh the list. diff --git a/lever-interface.lua b/lever-interface.lua index a8d8bcaa0..1be7361aa 100644 --- a/lever-interface.lua +++ b/lever-interface.lua @@ -1,228 +1,228 @@ --- List and pull levers - -local gui = require('gui') -local guidm = require('gui.dwarfmode') -local utils = require('utils') -local widgets = require('gui.widgets') - -local lever_script = reqscript('lever') - -local REFRESH_MS = 1000 - -local function get_levers() - local levers = {} - for _, building in ipairs(df.global.world.buildings.other.TRAP) do - if building.trap_type == df.trap_type.Lever then - table.insert(levers, building) - end - end - return levers -end - -local function get_lever_label(lever) - local status = (lever.state == 1) and 'Pulled' or 'Not Pulled' - local name = utils.getBuildingName(lever) - local queued = 0 - for _, job in ipairs(lever.jobs) do - if job.job_type == df.job_type.PullLever then - queued = queued + 1 - end - end - local queued_text = queued > 0 and (' (queued: %d)'):format(queued) or '' - return ('[%s] %s (#%d)%s'):format(status, name, lever.id, queued_text) -end - -local function get_queued_count(levers) - local queued = 0 - for _, lever in ipairs(levers) do - for _, job in ipairs(lever.jobs) do - if job.job_type == df.job_type.PullLever then - queued = queued + 1 - end - end - end - return queued -end - -LeverWindow = defclass(LeverWindow, widgets.Window) -LeverWindow.ATTRS{ - frame_title = 'Lever Tasks', - frame = {w=60, h=18, r=2}, -} - -function LeverWindow:init() - local _, screen_height = dfhack.screen.getWindowSize() - if screen_height then - self.frame.t = math.max(0, math.floor((screen_height - self.frame.h) / 2)) - end - self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS - self.filter_text = '' - self:addviews{ - widgets.EditField{ - view_id='search', - frame={t=0, l=0, r=0}, - label_text='Search: ', - on_change=self:callback('set_filter'), - }, - widgets.List{ - view_id='lever_list', - frame={t=1, l=0, r=0, b=4}, - on_submit=self:callback('queue_pull'), - on_select=self:callback('focus_lever'), - }, - widgets.Label{ - view_id='empty_message', - frame={t=1, l=0, r=0}, - text='No levers found.', - visible=false, - }, - widgets.HotkeyLabel{ - frame={b=3, l=0}, - label='Pull selected lever', - key='CUSTOM_P', - on_activate=self:callback('queue_pull'), - }, - widgets.HotkeyLabel{ - frame={b=2, l=0}, - label='Remove queued pulls', - key='CUSTOM_X', - on_activate=self:callback('remove_queued_pulls'), - }, - widgets.Label{ - view_id='queued_count', - frame={b=3, r=0}, - text='Queued pulls: 0', - auto_width=true, - }, - widgets.HotkeyLabel{ - frame={b=1, l=0}, - label='Refresh list', - key='CUSTOM_R', - on_activate=self:callback('refresh_list'), - }, - } - - self:refresh_list() -end - -function LeverWindow:set_filter(text) - self.filter_text = text or '' - self:refresh_list() -end - -function LeverWindow:refresh_list() - local list = self.subviews.lever_list - local selected_id - if list then - local _, selected = list:getSelected() - if selected and selected.data then - selected_id = selected.data.id - end - end - - local choices = {} - local levers = get_levers() - table.sort(levers, function(a, b) - if a.state == b.state then - return a.id < b.id - end - return a.state > b.state - end) - local filter = (self.filter_text or ''):lower() - local filtered_levers = {} - if filter == '' then - filtered_levers = levers - else - for _, lever in ipairs(levers) do - local name = utils.getBuildingName(lever) - if name:lower():find(filter, 1, true) then - table.insert(filtered_levers, lever) - end - end - end - local selected_idx = 1 - for idx, lever in ipairs(filtered_levers) do - table.insert(choices, {text=get_lever_label(lever), data=lever}) - if selected_id and lever.id == selected_id then - selected_idx = idx - end - end - list:setChoices(choices, selected_idx) - self.subviews.empty_message.visible = #choices == 0 - self.subviews.queued_count:setText(('Queued pulls: %d'):format(get_queued_count(levers))) -end - -function LeverWindow:queue_pull() - local _, choice = self.subviews.lever_list:getSelected() - if not choice then - return - end - lever_script.leverPullJob(choice.data, false) - self:refresh_list() -end - -function LeverWindow:remove_queued_pulls() - local _, choice = self.subviews.lever_list:getSelected() - if not choice then - return - end - local jobs = {} - for _, job in ipairs(choice.data.jobs) do - if job.job_type == df.job_type.PullLever then - table.insert(jobs, job) - end - end - for _, job in ipairs(jobs) do - dfhack.job.removeJob(job) - end - self:refresh_list() -end - -function LeverWindow:onRenderFrame(dc, rect) - LeverWindow.super.onRenderFrame(self, dc, rect) - - local list = self.subviews.lever_list - local hover_idx = list:getIdxUnderMouse() - if hover_idx and hover_idx ~= self.hover_index then - self.hover_index = hover_idx - list:setSelected(hover_idx) - local _, choice = list:getSelected() - if choice then - self:focus_lever(nil, choice) - end - end -end - -function LeverWindow:onRenderBody() - if dfhack.getTickCount() >= self.next_refresh_ms then - self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS - self:refresh_list() - end -end - -function LeverWindow:focus_lever(_, choice) - if not choice then - return - end - local lever = choice.data - local pos = {x=lever.centerx, y=lever.centery, z=lever.z} - dfhack.gui.revealInDwarfmodeMap(pos, true, true) - guidm.setCursorPos(pos) -end - -LeverScreen = defclass(LeverScreen, gui.ZScreen) -LeverScreen.ATTRS{focus_path='lever'} - -function LeverScreen:init() - self:addviews{LeverWindow{}} -end - -function LeverScreen:onDismiss() - view = nil -end - -if not dfhack.isMapLoaded() then - qerror('gui/lever requires a map to be loaded') -end - -view = view and view:raise() or LeverScreen{}:show() +-- List and pull levers + +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local utils = require('utils') +local widgets = require('gui.widgets') + +local lever_script = reqscript('lever') + +local REFRESH_MS = 1000 + +local function get_levers() + local levers = {} + for _, building in ipairs(df.global.world.buildings.other.TRAP) do + if building.trap_type == df.trap_type.Lever then + table.insert(levers, building) + end + end + return levers +end + +local function get_lever_label(lever) + local status = (lever.state == 1) and 'Pulled' or 'Not Pulled' + local name = utils.getBuildingName(lever) + local queued = 0 + for _, job in ipairs(lever.jobs) do + if job.job_type == df.job_type.PullLever then + queued = queued + 1 + end + end + local queued_text = queued > 0 and (' (queued: %d)'):format(queued) or '' + return ('[%s] %s (#%d)%s'):format(status, name, lever.id, queued_text) +end + +local function get_queued_count(levers) + local queued = 0 + for _, lever in ipairs(levers) do + for _, job in ipairs(lever.jobs) do + if job.job_type == df.job_type.PullLever then + queued = queued + 1 + end + end + end + return queued +end + +LeverWindow = defclass(LeverWindow, widgets.Window) +LeverWindow.ATTRS{ + frame_title = 'Lever Tasks', + frame = {w=60, h=18, r=2}, +} + +function LeverWindow:init() + local _, screen_height = dfhack.screen.getWindowSize() + if screen_height then + self.frame.t = math.max(0, math.floor((screen_height - self.frame.h) / 2)) + end + self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS + self.filter_text = '' + self:addviews{ + widgets.EditField{ + view_id='search', + frame={t=0, l=0, r=0}, + label_text='Search: ', + on_change=self:callback('set_filter'), + }, + widgets.List{ + view_id='lever_list', + frame={t=1, l=0, r=0, b=4}, + on_submit=self:callback('queue_pull'), + on_select=self:callback('focus_lever'), + }, + widgets.Label{ + view_id='empty_message', + frame={t=1, l=0, r=0}, + text='No levers found.', + visible=false, + }, + widgets.HotkeyLabel{ + frame={b=3, l=0}, + label='Pull selected lever', + key='CUSTOM_P', + on_activate=self:callback('queue_pull'), + }, + widgets.HotkeyLabel{ + frame={b=2, l=0}, + label='Remove queued pulls', + key='CUSTOM_X', + on_activate=self:callback('remove_queued_pulls'), + }, + widgets.Label{ + view_id='queued_count', + frame={b=3, r=0}, + text='Queued pulls: 0', + auto_width=true, + }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + label='Refresh list', + key='CUSTOM_R', + on_activate=self:callback('refresh_list'), + }, + } + + self:refresh_list() +end + +function LeverWindow:set_filter(text) + self.filter_text = text or '' + self:refresh_list() +end + +function LeverWindow:refresh_list() + local list = self.subviews.lever_list + local selected_id + if list then + local _, selected = list:getSelected() + if selected and selected.data then + selected_id = selected.data.id + end + end + + local choices = {} + local levers = get_levers() + table.sort(levers, function(a, b) + if a.state == b.state then + return a.id < b.id + end + return a.state > b.state + end) + local filter = (self.filter_text or ''):lower() + local filtered_levers = {} + if filter == '' then + filtered_levers = levers + else + for _, lever in ipairs(levers) do + local name = utils.getBuildingName(lever) + if name:lower():find(filter, 1, true) then + table.insert(filtered_levers, lever) + end + end + end + local selected_idx = 1 + for idx, lever in ipairs(filtered_levers) do + table.insert(choices, {text=get_lever_label(lever), data=lever}) + if selected_id and lever.id == selected_id then + selected_idx = idx + end + end + list:setChoices(choices, selected_idx) + self.subviews.empty_message.visible = #choices == 0 + self.subviews.queued_count:setText(('Queued pulls: %d'):format(get_queued_count(levers))) +end + +function LeverWindow:queue_pull() + local _, choice = self.subviews.lever_list:getSelected() + if not choice then + return + end + lever_script.leverPullJob(choice.data, false) + self:refresh_list() +end + +function LeverWindow:remove_queued_pulls() + local _, choice = self.subviews.lever_list:getSelected() + if not choice then + return + end + local jobs = {} + for _, job in ipairs(choice.data.jobs) do + if job.job_type == df.job_type.PullLever then + table.insert(jobs, job) + end + end + for _, job in ipairs(jobs) do + dfhack.job.removeJob(job) + end + self:refresh_list() +end + +function LeverWindow:onRenderFrame(dc, rect) + LeverWindow.super.onRenderFrame(self, dc, rect) + + local list = self.subviews.lever_list + local hover_idx = list:getIdxUnderMouse() + if hover_idx and hover_idx ~= self.hover_index then + self.hover_index = hover_idx + list:setSelected(hover_idx) + local _, choice = list:getSelected() + if choice then + self:focus_lever(nil, choice) + end + end +end + +function LeverWindow:onRenderBody() + if dfhack.getTickCount() >= self.next_refresh_ms then + self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS + self:refresh_list() + end +end + +function LeverWindow:focus_lever(_, choice) + if not choice then + return + end + local lever = choice.data + local pos = {x=lever.centerx, y=lever.centery, z=lever.z} + dfhack.gui.revealInDwarfmodeMap(pos, true, true) + guidm.setCursorPos(pos) +end + +LeverScreen = defclass(LeverScreen, gui.ZScreen) +LeverScreen.ATTRS{focus_path='lever'} + +function LeverScreen:init() + self:addviews{LeverWindow{}} +end + +function LeverScreen:onDismiss() + view = nil +end + +if not dfhack.isMapLoaded() then + qerror('gui/lever requires a map to be loaded') +end + +view = view and view:raise() or LeverScreen{}:show()