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/9] 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/9] 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/9] 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 a979b14b782fc42938bb3b47453dc5254da8923c Mon Sep 17 00:00:00 2001 From: unboundlopez <47876628+unboundlopez@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:01:10 -0600 Subject: [PATCH 4/9] Add files via upload --- order-search-filter.lua | 218 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 order-search-filter.lua diff --git a/order-search-filter.lua b/order-search-filter.lua new file mode 100644 index 000000000..0f012abd1 --- /dev/null +++ b/order-search-filter.lua @@ -0,0 +1,218 @@ +--@module = true +--[====[ + +gui/order-search-filter +======================= +Overlay search/filter panel for the manager work orders list. + +For manual testing, you can bind a hotkey in your ``dfhack*.init``:: + + keybinding add Alt+S@workquota gui/order-search-filter + +]====] + +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +local orders = df.global.world.manager_orders.all +local itemdefs = df.global.world.raws.itemdefs +local reactions = df.global.world.raws.reactions.reactions + +local meal_type_by_ingredient_count = { + [2] = 'easy', + [3] = 'fine', + [4] = 'lavish', +} + +local function make_order_material_desc(order, noun) + local desc = '' + if order.mat_type >= 0 then + local matinfo = dfhack.matinfo.decode(order.mat_type, order.mat_index) + if matinfo then + desc = desc .. ' ' .. matinfo:toString() + end + else + for k,v in pairs(order.material_category) do + if v then + desc = desc .. ' ' .. k + break + end + end + end + return desc .. ' ' .. noun +end + +local function make_order_desc(order) + if order.job_type == df.job_type.CustomReaction then + for _, reaction in ipairs(reactions) do + if reaction.code == order.reaction_name then + return reaction.name + end + end + return '' + elseif order.job_type == df.job_type.PrepareMeal then + local meal_type = meal_type_by_ingredient_count[order.mat_type] + if meal_type then + return 'prepare ' .. meal_type .. ' meal' + end + return 'prepare meal' + end + + local noun + if order.job_type == df.job_type.MakeArmor then + noun = itemdefs.armor[order.item_subtype].name + elseif order.job_type == df.job_type.MakeWeapon then + noun = itemdefs.weapons[order.item_subtype].name + elseif order.job_type == df.job_type.MakeShield then + noun = itemdefs.shields[order.item_subtype].name + elseif order.job_type == df.job_type.MakeAmmo then + noun = itemdefs.ammo[order.item_subtype].name + elseif order.job_type == df.job_type.MakeHelm then + noun = itemdefs.helms[order.item_subtype].name + elseif order.job_type == df.job_type.MakeGloves then + noun = itemdefs.gloves[order.item_subtype].name + elseif order.job_type == df.job_type.MakePants then + noun = itemdefs.pants[order.item_subtype].name + elseif order.job_type == df.job_type.MakeShoes then + noun = itemdefs.shoes[order.item_subtype].name + elseif order.job_type == df.job_type.MakeTool then + noun = itemdefs.tools[order.item_subtype].name + elseif order.job_type == df.job_type.MakeTrapComponent then + noun = itemdefs.trapcomps[order.item_subtype].name + elseif order.job_type == df.job_type.SmeltOre then + noun = 'ore' + else + noun = df.job_type.attrs[order.job_type].caption + end + return make_order_material_desc(order, noun) +end + +local function build_order_text(order) + local desc = make_order_desc(order) + local total = order.amount_total or 0 + local remaining = order.amount_left or total + if remaining ~= total then + return string.format('%s x%d (%d left)', desc, total, remaining) + end + return string.format('%s x%d', desc, total) +end + +OrderSearchFilter = defclass(OrderSearchFilter, overlay.OverlayWidget) +OrderSearchFilter.ATTRS{ + desc='Search and jump to work orders in the manager list.', + default_enabled=true, + default_pos={x=100, y=60}, + frame={w=34, h=3}, + overlay_onupdate_max_freq_seconds=1, + viewscreens='dwarfmode/Info/WORK_ORDERS/Default', +} + +function OrderSearchFilter:init() + self:addviews{ + widgets.Panel{ + subviews={ + widgets.EditField{ + view_id='filter', + frame={t=0, l=1, r=1}, + key='CUSTOM_ALT_S', + label_text='Filter: ', + on_change=self:callback('on_filter_change'), + }, + }, + }, + } +end + +function OrderSearchFilter:overlay_onupdate() + if self.filter_text then + self:apply_filter(self.filter_text) + end +end + +local function order_matches(filter_lc, order) + local text = build_order_text(order):lower() + return text:find(filter_lc, 1, true) ~= nil +end + +function OrderSearchFilter:snapshot_orders() + local snapshot = {} + for _, order in ipairs(orders) do + table.insert(snapshot, order) + end + return snapshot +end + +function OrderSearchFilter:rebuild_orders(new_orders) + for i = #orders - 1, 0, -1 do + orders:erase(i) + end + for _, order in ipairs(new_orders) do + orders:insert('#', order) + end + local mi = df.global.game.main_interface + if mi and mi.info and mi.info.work_orders then + mi.info.work_orders.scroll_position_work_orders = 0 + end +end + +function OrderSearchFilter:restore_orders() + if not self.unfiltered_orders then return end + local by_id = {} + for _, order in ipairs(self.unfiltered_orders) do + by_id[order.id] = order + end + for _, order in ipairs(orders) do + if not by_id[order.id] then + table.insert(self.unfiltered_orders, order) + by_id[order.id] = order + end + end + self:rebuild_orders(self.unfiltered_orders) + self.unfiltered_orders = nil +end + +function OrderSearchFilter:apply_filter(filter) + if filter == '' then + self:restore_orders() + return + end + if not self.unfiltered_orders then + self.unfiltered_orders = self:snapshot_orders() + end + local filter_lc = filter:lower() + local filtered = {} + for _, order in ipairs(self.unfiltered_orders) do + if order_matches(filter_lc, order) then + table.insert(filtered, order) + end + end + self:rebuild_orders(filtered) +end + +function OrderSearchFilter:on_filter_change(text) + self.filter_text = text + self:apply_filter(text) +end + +function OrderSearchFilter:onInput(keys) + if keys.SELECT then return false end + return OrderSearchFilter.super.onInput(self, keys) +end + +function OrderSearchFilter:overlay_ondisable() + self:restore_orders() +end + +OVERLAY_WIDGETS = { + order_search_filter=OrderSearchFilter, +} + +if dfhack_flags.module then + return +end + +if not dfhack.gui.matchFocusString('dwarfmode/Info/WORK_ORDERS/Default') then + qerror('This script must be run from the Work Orders screen.') +end + +overlay.overlay_command({'enable', 'order-search-filter.order_search_filter'}) From e88376469ec1230c895093bd29d4079cb015d106 Mon Sep 17 00:00:00 2001 From: unboundlopez <47876628+unboundlopez@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:01:25 -0600 Subject: [PATCH 5/9] Add files via upload --- docs/order-search-filter.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/order-search-filter.rst diff --git a/docs/order-search-filter.rst b/docs/order-search-filter.rst new file mode 100644 index 000000000..6cc24a94e --- /dev/null +++ b/docs/order-search-filter.rst @@ -0,0 +1,24 @@ +gui/order-search-filter +======================= + +.. dfhack-tool:: + :summary: Filter the Work Orders list from a compact overlay input. + :tags: fort interface + +This overlay adds a small filter field to the Work Orders screen. Typing text +filters the existing manager order list so only matching entries remain +visible. + +Usage +----- + +:: + + gui/order-search-filter + +Overlay +------- + +When the Work Orders screen is open, press :kbd:`Alt`:kbd:`S` to focus the +filter field. As you type, the list of work orders updates to show only the +matching entries. Clearing the filter restores the full list. From 7b32b98bfc2ddb889f6e31b65d2d5942c67aa343 Mon Sep 17 00:00:00 2001 From: unboundlopez <47876628+unboundlopez@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:02:01 -0600 Subject: [PATCH 6/9] Update order-search-filter.rst --- docs/order-search-filter.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/order-search-filter.rst b/docs/order-search-filter.rst index 6cc24a94e..39811c6e1 100644 --- a/docs/order-search-filter.rst +++ b/docs/order-search-filter.rst @@ -1,4 +1,4 @@ -gui/order-search-filter +order-search-filter ======================= .. dfhack-tool:: @@ -22,3 +22,4 @@ Overlay When the Work Orders screen is open, press :kbd:`Alt`:kbd:`S` to focus the filter field. As you type, the list of work orders updates to show only the matching entries. Clearing the filter restores the full list. + From 75a5e6035783bd0fb6f7932c5f1515c7f028f858 Mon Sep 17 00:00:00 2001 From: unboundlopez <47876628+unboundlopez@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:02:44 -0600 Subject: [PATCH 7/9] Update order-search-filter.lua --- order-search-filter.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/order-search-filter.lua b/order-search-filter.lua index 0f012abd1..ccac51782 100644 --- a/order-search-filter.lua +++ b/order-search-filter.lua @@ -1,13 +1,13 @@ --@module = true --[====[ -gui/order-search-filter +order-search-filter ======================= Overlay search/filter panel for the manager work orders list. For manual testing, you can bind a hotkey in your ``dfhack*.init``:: - keybinding add Alt+S@workquota gui/order-search-filter + keybinding add Alt+S@workquota order-search-filter ]====] @@ -216,3 +216,4 @@ if not dfhack.gui.matchFocusString('dwarfmode/Info/WORK_ORDERS/Default') then end overlay.overlay_command({'enable', 'order-search-filter.order_search_filter'}) + From 0ed6e181e4c533d3930cc7a68abdd4fbab618f43 Mon Sep 17 00:00:00 2001 From: unboundlopez <47876628+unboundlopez@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:04:23 -0600 Subject: [PATCH 8/9] Update order-search-filter.rst --- docs/order-search-filter.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/order-search-filter.rst b/docs/order-search-filter.rst index 39811c6e1..1c01e88cd 100644 --- a/docs/order-search-filter.rst +++ b/docs/order-search-filter.rst @@ -14,7 +14,7 @@ Usage :: - gui/order-search-filter + order-search-filter Overlay ------- @@ -23,3 +23,4 @@ When the Work Orders screen is open, press :kbd:`Alt`:kbd:`S` to focus the filter field. As you type, the list of work orders updates to show only the matching entries. Clearing the filter restores the full list. + From d47070b06d9964b998833d27cd41bcb7b78ff068 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 06:06:56 +0000 Subject: [PATCH 9/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/order-search-filter.rst | 50 ++-- order-search-filter.lua | 437 +++++++++++++++++------------------ 2 files changed, 242 insertions(+), 245 deletions(-) diff --git a/docs/order-search-filter.rst b/docs/order-search-filter.rst index 1c01e88cd..6f45e0d60 100644 --- a/docs/order-search-filter.rst +++ b/docs/order-search-filter.rst @@ -1,26 +1,24 @@ -order-search-filter -======================= - -.. dfhack-tool:: - :summary: Filter the Work Orders list from a compact overlay input. - :tags: fort interface - -This overlay adds a small filter field to the Work Orders screen. Typing text -filters the existing manager order list so only matching entries remain -visible. - -Usage ------ - -:: - - order-search-filter - -Overlay -------- - -When the Work Orders screen is open, press :kbd:`Alt`:kbd:`S` to focus the -filter field. As you type, the list of work orders updates to show only the -matching entries. Clearing the filter restores the full list. - - +order-search-filter +======================= + +.. dfhack-tool:: + :summary: Filter the Work Orders list from a compact overlay input. + :tags: fort interface + +This overlay adds a small filter field to the Work Orders screen. Typing text +filters the existing manager order list so only matching entries remain +visible. + +Usage +----- + +:: + + order-search-filter + +Overlay +------- + +When the Work Orders screen is open, press :kbd:`Alt`:kbd:`S` to focus the +filter field. As you type, the list of work orders updates to show only the +matching entries. Clearing the filter restores the full list. diff --git a/order-search-filter.lua b/order-search-filter.lua index ccac51782..6ef372e51 100644 --- a/order-search-filter.lua +++ b/order-search-filter.lua @@ -1,219 +1,218 @@ ---@module = true ---[====[ - -order-search-filter -======================= -Overlay search/filter panel for the manager work orders list. - -For manual testing, you can bind a hotkey in your ``dfhack*.init``:: - - keybinding add Alt+S@workquota order-search-filter - -]====] - -local overlay = require('plugins.overlay') -local widgets = require('gui.widgets') - -local orders = df.global.world.manager_orders.all -local itemdefs = df.global.world.raws.itemdefs -local reactions = df.global.world.raws.reactions.reactions - -local meal_type_by_ingredient_count = { - [2] = 'easy', - [3] = 'fine', - [4] = 'lavish', -} - -local function make_order_material_desc(order, noun) - local desc = '' - if order.mat_type >= 0 then - local matinfo = dfhack.matinfo.decode(order.mat_type, order.mat_index) - if matinfo then - desc = desc .. ' ' .. matinfo:toString() - end - else - for k,v in pairs(order.material_category) do - if v then - desc = desc .. ' ' .. k - break - end - end - end - return desc .. ' ' .. noun -end - -local function make_order_desc(order) - if order.job_type == df.job_type.CustomReaction then - for _, reaction in ipairs(reactions) do - if reaction.code == order.reaction_name then - return reaction.name - end - end - return '' - elseif order.job_type == df.job_type.PrepareMeal then - local meal_type = meal_type_by_ingredient_count[order.mat_type] - if meal_type then - return 'prepare ' .. meal_type .. ' meal' - end - return 'prepare meal' - end - - local noun - if order.job_type == df.job_type.MakeArmor then - noun = itemdefs.armor[order.item_subtype].name - elseif order.job_type == df.job_type.MakeWeapon then - noun = itemdefs.weapons[order.item_subtype].name - elseif order.job_type == df.job_type.MakeShield then - noun = itemdefs.shields[order.item_subtype].name - elseif order.job_type == df.job_type.MakeAmmo then - noun = itemdefs.ammo[order.item_subtype].name - elseif order.job_type == df.job_type.MakeHelm then - noun = itemdefs.helms[order.item_subtype].name - elseif order.job_type == df.job_type.MakeGloves then - noun = itemdefs.gloves[order.item_subtype].name - elseif order.job_type == df.job_type.MakePants then - noun = itemdefs.pants[order.item_subtype].name - elseif order.job_type == df.job_type.MakeShoes then - noun = itemdefs.shoes[order.item_subtype].name - elseif order.job_type == df.job_type.MakeTool then - noun = itemdefs.tools[order.item_subtype].name - elseif order.job_type == df.job_type.MakeTrapComponent then - noun = itemdefs.trapcomps[order.item_subtype].name - elseif order.job_type == df.job_type.SmeltOre then - noun = 'ore' - else - noun = df.job_type.attrs[order.job_type].caption - end - return make_order_material_desc(order, noun) -end - -local function build_order_text(order) - local desc = make_order_desc(order) - local total = order.amount_total or 0 - local remaining = order.amount_left or total - if remaining ~= total then - return string.format('%s x%d (%d left)', desc, total, remaining) - end - return string.format('%s x%d', desc, total) -end - -OrderSearchFilter = defclass(OrderSearchFilter, overlay.OverlayWidget) -OrderSearchFilter.ATTRS{ - desc='Search and jump to work orders in the manager list.', - default_enabled=true, - default_pos={x=100, y=60}, - frame={w=34, h=3}, - overlay_onupdate_max_freq_seconds=1, - viewscreens='dwarfmode/Info/WORK_ORDERS/Default', -} - -function OrderSearchFilter:init() - self:addviews{ - widgets.Panel{ - subviews={ - widgets.EditField{ - view_id='filter', - frame={t=0, l=1, r=1}, - key='CUSTOM_ALT_S', - label_text='Filter: ', - on_change=self:callback('on_filter_change'), - }, - }, - }, - } -end - -function OrderSearchFilter:overlay_onupdate() - if self.filter_text then - self:apply_filter(self.filter_text) - end -end - -local function order_matches(filter_lc, order) - local text = build_order_text(order):lower() - return text:find(filter_lc, 1, true) ~= nil -end - -function OrderSearchFilter:snapshot_orders() - local snapshot = {} - for _, order in ipairs(orders) do - table.insert(snapshot, order) - end - return snapshot -end - -function OrderSearchFilter:rebuild_orders(new_orders) - for i = #orders - 1, 0, -1 do - orders:erase(i) - end - for _, order in ipairs(new_orders) do - orders:insert('#', order) - end - local mi = df.global.game.main_interface - if mi and mi.info and mi.info.work_orders then - mi.info.work_orders.scroll_position_work_orders = 0 - end -end - -function OrderSearchFilter:restore_orders() - if not self.unfiltered_orders then return end - local by_id = {} - for _, order in ipairs(self.unfiltered_orders) do - by_id[order.id] = order - end - for _, order in ipairs(orders) do - if not by_id[order.id] then - table.insert(self.unfiltered_orders, order) - by_id[order.id] = order - end - end - self:rebuild_orders(self.unfiltered_orders) - self.unfiltered_orders = nil -end - -function OrderSearchFilter:apply_filter(filter) - if filter == '' then - self:restore_orders() - return - end - if not self.unfiltered_orders then - self.unfiltered_orders = self:snapshot_orders() - end - local filter_lc = filter:lower() - local filtered = {} - for _, order in ipairs(self.unfiltered_orders) do - if order_matches(filter_lc, order) then - table.insert(filtered, order) - end - end - self:rebuild_orders(filtered) -end - -function OrderSearchFilter:on_filter_change(text) - self.filter_text = text - self:apply_filter(text) -end - -function OrderSearchFilter:onInput(keys) - if keys.SELECT then return false end - return OrderSearchFilter.super.onInput(self, keys) -end - -function OrderSearchFilter:overlay_ondisable() - self:restore_orders() -end - -OVERLAY_WIDGETS = { - order_search_filter=OrderSearchFilter, -} - -if dfhack_flags.module then - return -end - -if not dfhack.gui.matchFocusString('dwarfmode/Info/WORK_ORDERS/Default') then - qerror('This script must be run from the Work Orders screen.') -end - -overlay.overlay_command({'enable', 'order-search-filter.order_search_filter'}) - +--@module = true +--[====[ + +order-search-filter +======================= +Overlay search/filter panel for the manager work orders list. + +For manual testing, you can bind a hotkey in your ``dfhack*.init``:: + + keybinding add Alt+S@workquota order-search-filter + +]====] + +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +local orders = df.global.world.manager_orders.all +local itemdefs = df.global.world.raws.itemdefs +local reactions = df.global.world.raws.reactions.reactions + +local meal_type_by_ingredient_count = { + [2] = 'easy', + [3] = 'fine', + [4] = 'lavish', +} + +local function make_order_material_desc(order, noun) + local desc = '' + if order.mat_type >= 0 then + local matinfo = dfhack.matinfo.decode(order.mat_type, order.mat_index) + if matinfo then + desc = desc .. ' ' .. matinfo:toString() + end + else + for k,v in pairs(order.material_category) do + if v then + desc = desc .. ' ' .. k + break + end + end + end + return desc .. ' ' .. noun +end + +local function make_order_desc(order) + if order.job_type == df.job_type.CustomReaction then + for _, reaction in ipairs(reactions) do + if reaction.code == order.reaction_name then + return reaction.name + end + end + return '' + elseif order.job_type == df.job_type.PrepareMeal then + local meal_type = meal_type_by_ingredient_count[order.mat_type] + if meal_type then + return 'prepare ' .. meal_type .. ' meal' + end + return 'prepare meal' + end + + local noun + if order.job_type == df.job_type.MakeArmor then + noun = itemdefs.armor[order.item_subtype].name + elseif order.job_type == df.job_type.MakeWeapon then + noun = itemdefs.weapons[order.item_subtype].name + elseif order.job_type == df.job_type.MakeShield then + noun = itemdefs.shields[order.item_subtype].name + elseif order.job_type == df.job_type.MakeAmmo then + noun = itemdefs.ammo[order.item_subtype].name + elseif order.job_type == df.job_type.MakeHelm then + noun = itemdefs.helms[order.item_subtype].name + elseif order.job_type == df.job_type.MakeGloves then + noun = itemdefs.gloves[order.item_subtype].name + elseif order.job_type == df.job_type.MakePants then + noun = itemdefs.pants[order.item_subtype].name + elseif order.job_type == df.job_type.MakeShoes then + noun = itemdefs.shoes[order.item_subtype].name + elseif order.job_type == df.job_type.MakeTool then + noun = itemdefs.tools[order.item_subtype].name + elseif order.job_type == df.job_type.MakeTrapComponent then + noun = itemdefs.trapcomps[order.item_subtype].name + elseif order.job_type == df.job_type.SmeltOre then + noun = 'ore' + else + noun = df.job_type.attrs[order.job_type].caption + end + return make_order_material_desc(order, noun) +end + +local function build_order_text(order) + local desc = make_order_desc(order) + local total = order.amount_total or 0 + local remaining = order.amount_left or total + if remaining ~= total then + return string.format('%s x%d (%d left)', desc, total, remaining) + end + return string.format('%s x%d', desc, total) +end + +OrderSearchFilter = defclass(OrderSearchFilter, overlay.OverlayWidget) +OrderSearchFilter.ATTRS{ + desc='Search and jump to work orders in the manager list.', + default_enabled=true, + default_pos={x=100, y=60}, + frame={w=34, h=3}, + overlay_onupdate_max_freq_seconds=1, + viewscreens='dwarfmode/Info/WORK_ORDERS/Default', +} + +function OrderSearchFilter:init() + self:addviews{ + widgets.Panel{ + subviews={ + widgets.EditField{ + view_id='filter', + frame={t=0, l=1, r=1}, + key='CUSTOM_ALT_S', + label_text='Filter: ', + on_change=self:callback('on_filter_change'), + }, + }, + }, + } +end + +function OrderSearchFilter:overlay_onupdate() + if self.filter_text then + self:apply_filter(self.filter_text) + end +end + +local function order_matches(filter_lc, order) + local text = build_order_text(order):lower() + return text:find(filter_lc, 1, true) ~= nil +end + +function OrderSearchFilter:snapshot_orders() + local snapshot = {} + for _, order in ipairs(orders) do + table.insert(snapshot, order) + end + return snapshot +end + +function OrderSearchFilter:rebuild_orders(new_orders) + for i = #orders - 1, 0, -1 do + orders:erase(i) + end + for _, order in ipairs(new_orders) do + orders:insert('#', order) + end + local mi = df.global.game.main_interface + if mi and mi.info and mi.info.work_orders then + mi.info.work_orders.scroll_position_work_orders = 0 + end +end + +function OrderSearchFilter:restore_orders() + if not self.unfiltered_orders then return end + local by_id = {} + for _, order in ipairs(self.unfiltered_orders) do + by_id[order.id] = order + end + for _, order in ipairs(orders) do + if not by_id[order.id] then + table.insert(self.unfiltered_orders, order) + by_id[order.id] = order + end + end + self:rebuild_orders(self.unfiltered_orders) + self.unfiltered_orders = nil +end + +function OrderSearchFilter:apply_filter(filter) + if filter == '' then + self:restore_orders() + return + end + if not self.unfiltered_orders then + self.unfiltered_orders = self:snapshot_orders() + end + local filter_lc = filter:lower() + local filtered = {} + for _, order in ipairs(self.unfiltered_orders) do + if order_matches(filter_lc, order) then + table.insert(filtered, order) + end + end + self:rebuild_orders(filtered) +end + +function OrderSearchFilter:on_filter_change(text) + self.filter_text = text + self:apply_filter(text) +end + +function OrderSearchFilter:onInput(keys) + if keys.SELECT then return false end + return OrderSearchFilter.super.onInput(self, keys) +end + +function OrderSearchFilter:overlay_ondisable() + self:restore_orders() +end + +OVERLAY_WIDGETS = { + order_search_filter=OrderSearchFilter, +} + +if dfhack_flags.module then + return +end + +if not dfhack.gui.matchFocusString('dwarfmode/Info/WORK_ORDERS/Default') then + qerror('This script must be run from the Work Orders screen.') +end + +overlay.overlay_command({'enable', 'order-search-filter.order_search_filter'})