From cd6ce941e7fb94a94c463c2368e92b78b4829f98 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Thu, 8 Jan 2026 19:20:14 +0100 Subject: [PATCH 1/7] Add output and labels to quickcmd --- changelog.txt | 1 + gui/quickcmd.lua | 166 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 148 insertions(+), 19 deletions(-) diff --git a/changelog.txt b/changelog.txt index d9d0d44ea0..611f2d28ab 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools ## New Features +- `gui/quickcmd`: added custom command names and option to display command output ## Fixes diff --git a/gui/quickcmd.lua b/gui/quickcmd.lua index c1e9866988..21ae09075f 100644 --- a/gui/quickcmd.lua +++ b/gui/quickcmd.lua @@ -36,15 +36,30 @@ local widgets = require('gui.widgets') local CONFIG_FILE = 'dfhack-config/quickcmd.json' local HOTKEYWIDTH = 7 +local OUTWIDTH = 4 local HOTKEYS = 'asdfghjklqwertyuiopzxcvbnm' +local function save_commands(data) + json.encode_file(data, CONFIG_FILE) +end + local function load_commands() local ok, data = pcall(json.decode_file, CONFIG_FILE) - return ok and data or {} -end + if not ok then return {} end -local function save_commands(data) - json.encode_file(data, CONFIG_FILE) + -- Migrate old string format to new object format + local migrated = false + for i, cmd in ipairs(data) do + if type(cmd) == 'string' then + data[i] = {command = cmd, name = '', show_output = false} + migrated = true + end + end + if migrated then + save_commands(data) + end + + return data end QCMDDialog = defclass(QCMDDialog, widgets.Window) @@ -61,12 +76,12 @@ function QCMDDialog:init(info) self:addviews{ widgets.Label{ frame={t=0}, - text={{text='Hotkey', width=HOTKEYWIDTH}, ' Command'}, + text={{text='Hotkey', width=HOTKEYWIDTH}, {text='Out', width=OUTWIDTH}, 'Name/Command'}, visible=function() return #self.commands > 0 end, }, widgets.List{ view_id='list', - frame={t=2, b=3}, + frame={t=2, b=4}, on_submit=self:callback('submit'), }, widgets.Label{ @@ -75,53 +90,132 @@ function QCMDDialog:init(info) visible=function() return #self.commands == 0 end, }, widgets.HotkeyLabel{ - frame={b=1, l=0}, + frame={b=2, l=0}, key='CUSTOM_SHIFT_A', label='Add command', auto_width=true, on_activate=self:callback('onAddCommand'), }, widgets.HotkeyLabel{ - frame={b=1, l=19}, + frame={b=2, l=19}, key='CUSTOM_SHIFT_D', label='Delete command', auto_width=true, on_activate=self:callback('onDelCommand'), }, widgets.HotkeyLabel{ - frame={b=0, l=0}, + frame={b=1, l=0}, key='CUSTOM_SHIFT_E', label='Edit command', auto_width=true, on_activate=self:callback('onEditCommand'), }, + widgets.HotkeyLabel{ + frame={b=1, l=19}, + key='CUSTOM_SHIFT_N', + label='Edit name', + auto_width=true, + on_activate=self:callback('onSetName'), + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + key='CUSTOM_SHIFT_O', + label='Capture output', + auto_width=true, + on_activate=self:callback('onToggleOutput'), + }, } self:updateList() end function QCMDDialog:submit(idx, choice) + local cmd_obj = self.commands[idx] + + if cmd_obj.show_output then + self:showCommandOutput(cmd_obj.command, cmd_obj.name) + else + local screen = self.parent_view + dfhack.screen.hideGuard(screen, function() + dfhack.run_command(cmd_obj.command) + end) + screen:dismiss() + end +end + +function QCMDDialog:showCommandOutput(command, name) + local output = dfhack.run_command_silent(command) + + -- Dismiss the quickcmd dialog before showing output local screen = self.parent_view - dfhack.screen.hideGuard(screen, function() - dfhack.run_command(choice.command) - end) screen:dismiss() + + local OutputDialog = defclass(OutputDialog, gui.ZScreenModal) + OutputDialog.ATTRS{ + focus_path='quickcmd_output', + command='', + name='', + output='', + } + + function OutputDialog:init() + local title + if self.name and self.name ~= '' then + title = self.name .. ': ' .. self.command + else + title = self.command + end + self:addviews{ + widgets.Window{ + frame_title=title, + frame={w=80, h=25}, + resizable=true, + resize_min={h=10, w=40}, + subviews={ + widgets.WrappedLabel{ + view_id='output', + frame={t=0, l=0, r=0, b=2}, + text_to_wrap=self.output or 'No output', + scroll_keys=widgets.STANDARDSCROLL, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + key='LEAVESCREEN', + label='Close', + auto_width=true, + on_activate=self:callback('dismiss'), + }, + } + } + } + end + + if #output == 0 then + output = 'Command finished successfully' + end + + OutputDialog{command=command, name=name, output=output}:show() end function QCMDDialog:updateList() -- Build the list entries. local choices = {} - for i,command in ipairs(self.commands) do + for i,cmd_obj in ipairs(self.commands) do -- Get the hotkey for this entry. local hotkey = nil if i <= HOTKEYS:len() then hotkey = HOTKEYS:sub(i, i) end + -- Display name if set, otherwise display command + local display_text = cmd_obj.name and cmd_obj.name ~= '' and cmd_obj.name or cmd_obj.command + -- Store the entry. table.insert(choices, { - text={{text=hotkey or '', width=HOTKEYWIDTH}, ' ', command}, - command=command, + text={{text=hotkey or '', width=HOTKEYWIDTH}, {text=cmd_obj.show_output and '[X]' or '[ ]', width=OUTWIDTH}, display_text}, + command=cmd_obj.command, + name=cmd_obj.name, + show_output=cmd_obj.show_output, hotkey=hotkey and ('CUSTOM_' .. hotkey:upper()) or '', }) end @@ -148,7 +242,7 @@ function QCMDDialog:onAddCommand() COLOR_GREEN, '', function(command) - table.insert(self.commands, command) + table.insert(self.commands, {command=command, name='', show_output=false}) save_commands(self.commands) self:updateList() end @@ -165,7 +259,7 @@ function QCMDDialog:onDelCommand() -- Prompt for confirmation. dlg.showYesNoPrompt( 'Delete command', - 'Are you sure you want to delete this command: ' .. NEWLINE .. item.command, + 'Are you sure you want to delete this command: ' .. NEWLINE .. self.commands[index].command, COLOR_GREEN, function() table.remove(self.commands, index) @@ -187,15 +281,49 @@ function QCMDDialog:onEditCommand() 'Edit command', 'Enter command:', COLOR_GREEN, - item.command, + self.commands[index].command, function(command) - self.commands[index] = command + self.commands[index].command = command + save_commands(self.commands) + self:updateList() + end + ) +end + +function QCMDDialog:onSetName() + -- Get the selected command. + local index, item = self.subviews.list:getSelected() + if not item then + return + end + + -- Prompt for new name. + dlg.showInputPrompt( + 'Set name', + 'Enter name:', + COLOR_GREEN, + self.commands[index].name or '', + function(name) + self.commands[index].name = name save_commands(self.commands) self:updateList() end ) end +function QCMDDialog:onToggleOutput() + -- Get the selected command. + local index, item = self.subviews.list:getSelected() + if not item then + return + end + + -- Toggle the show_output flag. + self.commands[index].show_output = not self.commands[index].show_output + save_commands(self.commands) + self:updateList() +end + QCMDScreen = defclass(QCMDScreen, gui.ZScreen) QCMDScreen.ATTRS { focus_path='quickcmd', From 626a3a469c59de61dc7fc86993e40e9e31c6bde5 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Fri, 9 Jan 2026 19:21:39 +0100 Subject: [PATCH 2/7] Use separate config file for forward compatibility --- gui/quickcmd.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gui/quickcmd.lua b/gui/quickcmd.lua index 21ae09075f..bc86d89e9d 100644 --- a/gui/quickcmd.lua +++ b/gui/quickcmd.lua @@ -34,7 +34,8 @@ local json = require('json') local gui = require('gui') local widgets = require('gui.widgets') -local CONFIG_FILE = 'dfhack-config/quickcmd.json' +local CONFIG_FILE_OLD = 'dfhack-config/quickcmd.json' +local CONFIG_FILE = 'dfhack-config/quickcmd-v2.json' local HOTKEYWIDTH = 7 local OUTWIDTH = 4 local HOTKEYS = 'asdfghjklqwertyuiopzxcvbnm' @@ -44,20 +45,20 @@ local function save_commands(data) end local function load_commands() + -- Try to load from new config file first local ok, data = pcall(json.decode_file, CONFIG_FILE) + if ok then return data end + + -- New file doesn't exist or is invalid - try old file for migration + ok, data = pcall(json.decode_file, CONFIG_FILE_OLD) if not ok then return {} end - -- Migrate old string format to new object format - local migrated = false + -- Migrate old string format to new object format (in-memory only) for i, cmd in ipairs(data) do if type(cmd) == 'string' then data[i] = {command = cmd, name = '', show_output = false} - migrated = true end end - if migrated then - save_commands(data) - end return data end From 0e34a49471cb5768542b282bf981a51925b9ad83 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 21 Feb 2026 18:40:47 +0100 Subject: [PATCH 3/7] Backup file instead of version file --- gui/quickcmd.lua | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/gui/quickcmd.lua b/gui/quickcmd.lua index bc86d89e9d..0802ddd67b 100644 --- a/gui/quickcmd.lua +++ b/gui/quickcmd.lua @@ -34,33 +34,47 @@ local json = require('json') local gui = require('gui') local widgets = require('gui.widgets') -local CONFIG_FILE_OLD = 'dfhack-config/quickcmd.json' -local CONFIG_FILE = 'dfhack-config/quickcmd-v2.json' +local CONFIG_FILE_BACKUP = 'dfhack-config/quickcmd.json.bak' +local CONFIG_FILE = 'dfhack-config/quickcmd.json' local HOTKEYWIDTH = 7 local OUTWIDTH = 4 local HOTKEYS = 'asdfghjklqwertyuiopzxcvbnm' local function save_commands(data) - json.encode_file(data, CONFIG_FILE) + json.encode_file({version=2, commands=data}, CONFIG_FILE) end -local function load_commands() - -- Try to load from new config file first - local ok, data = pcall(json.decode_file, CONFIG_FILE) - if ok then return data end - - -- New file doesn't exist or is invalid - try old file for migration - ok, data = pcall(json.decode_file, CONFIG_FILE_OLD) - if not ok then return {} end +local function migrate_to_v2(data) + json.encode_file(data, CONFIG_FILE_BACKUP) - -- Migrate old string format to new object format (in-memory only) + local commands = {} for i, cmd in ipairs(data) do if type(cmd) == 'string' then - data[i] = {command = cmd, name = '', show_output = false} + table.insert(commands, {command = cmd, name = '', show_output = false}) + else + table.insert(commands, cmd) + end + end + + save_commands(commands) + return commands +end + +local function load_commands() + local ok, data = pcall(json.decode_file, CONFIG_FILE) + if ok then + if type(data) == 'table' and data.version then + if data.version == 2 then + return data.commands or {} + end + end + -- Old format: array of strings + if type(data) == 'table' and #data > 0 then + return migrate_to_v2(data) end end - return data + return {} end QCMDDialog = defclass(QCMDDialog, widgets.Window) From 4debfc2d98764a3ac9d45c7316c175f3995fda48 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 21 Feb 2026 19:23:21 +0100 Subject: [PATCH 4/7] Canonical value --- gui/quickcmd.lua | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/gui/quickcmd.lua b/gui/quickcmd.lua index 0802ddd67b..02029ea065 100644 --- a/gui/quickcmd.lua +++ b/gui/quickcmd.lua @@ -50,9 +50,7 @@ local function migrate_to_v2(data) local commands = {} for i, cmd in ipairs(data) do if type(cmd) == 'string' then - table.insert(commands, {command = cmd, name = '', show_output = false}) - else - table.insert(commands, cmd) + table.insert(commands, {command = cmd, name = nil, show_output = false}) end end @@ -175,7 +173,7 @@ function QCMDDialog:showCommandOutput(command, name) function OutputDialog:init() local title - if self.name and self.name ~= '' then + if self.name then title = self.name .. ': ' .. self.command else title = self.command @@ -223,7 +221,7 @@ function QCMDDialog:updateList() end -- Display name if set, otherwise display command - local display_text = cmd_obj.name and cmd_obj.name ~= '' and cmd_obj.name or cmd_obj.command + local display_text = cmd_obj.name or cmd_obj.command -- Store the entry. table.insert(choices, { @@ -257,7 +255,7 @@ function QCMDDialog:onAddCommand() COLOR_GREEN, '', function(command) - table.insert(self.commands, {command=command, name='', show_output=false}) + table.insert(self.commands, {command=command, name=nil, show_output=false}) save_commands(self.commands) self:updateList() end @@ -319,7 +317,7 @@ function QCMDDialog:onSetName() COLOR_GREEN, self.commands[index].name or '', function(name) - self.commands[index].name = name + self.commands[index].name = name ~= '' and name or nil save_commands(self.commands) self:updateList() end From c1fbdde1de617ddcea5c2885b0281fcec1055c46 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 21 Feb 2026 20:29:53 +0100 Subject: [PATCH 5/7] Title fix --- gui/quickcmd.lua | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/gui/quickcmd.lua b/gui/quickcmd.lua index 02029ea065..8f763e548f 100644 --- a/gui/quickcmd.lua +++ b/gui/quickcmd.lua @@ -172,12 +172,8 @@ function QCMDDialog:showCommandOutput(command, name) } function OutputDialog:init() - local title - if self.name then - title = self.name .. ': ' .. self.command - else - title = self.command - end + local title = ('%s%s'):format(self.name ~= '' and self.name .. ': ' or '', self.command) + self:addviews{ widgets.Window{ frame_title=title, From db5d288f75cb07fc632529590e194e86fc0ea686 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 21 Feb 2026 20:44:38 +0100 Subject: [PATCH 6/7] Change modal to default zscreen --- gui/quickcmd.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/quickcmd.lua b/gui/quickcmd.lua index 8f763e548f..37f0c8960e 100644 --- a/gui/quickcmd.lua +++ b/gui/quickcmd.lua @@ -163,7 +163,7 @@ function QCMDDialog:showCommandOutput(command, name) local screen = self.parent_view screen:dismiss() - local OutputDialog = defclass(OutputDialog, gui.ZScreenModal) + local OutputDialog = defclass(OutputDialog, gui.ZScreen) OutputDialog.ATTRS{ focus_path='quickcmd_output', command='', From 1c13683a3c72acbffbff76c6f06fd575160e2647 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 21 Feb 2026 21:00:43 +0100 Subject: [PATCH 7/7] Trailing space --- gui/quickcmd.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/quickcmd.lua b/gui/quickcmd.lua index 37f0c8960e..5da0232a7f 100644 --- a/gui/quickcmd.lua +++ b/gui/quickcmd.lua @@ -173,7 +173,7 @@ function QCMDDialog:showCommandOutput(command, name) function OutputDialog:init() local title = ('%s%s'):format(self.name ~= '' and self.name .. ': ' or '', self.command) - + self:addviews{ widgets.Window{ frame_title=title,