--[[ @Title: Backup and Restore Family Historian Settings via Windows @Type: Standard @Author: Mark Draper @Version: 3.0 @LastUpdated: 2 Jun 2026 @Licence: This plugin is copyright (c) 2026 Mark Draper and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence) @Description: Backup and restore Family Historian custom settings. This plugin does all the copying directly within Windows rather than in the plugin. This gives much faster and more convenient operation, as well as providing the option to implement more advanced features such as single-click rapid backup or automated backup. ]] --[[ Version 1.0 (May 2022) Initial Store version Version 1.1 (May 2022) Emulator-compatible version Xcopy for emulator copy Clearer label for restore option Fixed bug with WOW64Node in Advanced mode Version 1.1.1 (May 2022) Xcopy options tweaked Does not overwrite its own settings or temporary batch files Version 1.1.2 (May 2022) ASCII check modified to permit FH5 Version 1.2 (Jan 2023) Checks only one instance running before restoring to prevent potential data loss Various minor code tidies and rationalisations Version 1.2.1 (Feb 2023) Corrects instance count when running under FH6 (Fh vs fh!) Version 1.3 (Sep 2024) Corrects restore operation in emulator and new emulator check Now fully Unicode compatible (FSO rather than lfs) Moves temporary script files to tmp folder and improved ini file format Now edits Registry file in FH6 as well so fully functional Improved user dialogs and menu button labels Various code optimisations and minor tweaks Version 2.0 (Dec 2024) Simplified restore without overwrite option (always restores in full) Simplified main menu reflecting this change Enhanced menu pop-up prompts Supports multi-monitor extended desktops Checks existing backups for completeness before updating or restoring Various corrections and enhancements to Unicode support Backup script modified to call 64-bit Registry key specifically if running in Advanced mode (avoids potential issues with missing backup file when called directly from Windows) Version 2.1 (May 2025) Modified 'three test' emulator detection CPU architecture detection ARM64 warning Version 2.1.1 (Jul 2025) Corrected ARM64 warning Version 2.1.4 (Aug 2025) Generalised ARM64 warning Minor tidy-up of old code Version 3.0 (Jun 2026) Simplified for easier maintenance by removing emulator support and legacy compatibility Various other code refinements ]] fhInitialise(5,0,0,'save_required') require 'luacom' require('iuplua') -- warn if emulator detected if os.getenv('WINEPREFIX') then local msg = 'Family Historian appears to be running under an emulator. This is not supported by the ' .. 'current plugin version.\n\nSee the plugin help for a fuller discussion of backing up FH ' .. 'settings in a WINE emulator' fhMessageBox(msg, 'OK', 'MB_ICONSTOP') return end if fhGetAppVersion() > 6 then fh = require('fhUtils') fh.setIupDefaults() else iup.SetGlobal('CUSTOMQUITMESSAGE','YES') iup.SetGlobal('UTF8MODE','YES') end iup.SetGlobal('UTF8MODE_FILE','YES') FSO = luacom.CreateObject('Scripting.FileSystemObject') -- ********************************************************************* function main() local MainForm if not GetOptions() then return end -- get plugin options -- set main window static controls and options local Heading = 'This plugin backs up and restores Family Historian settings using Windows commands.\n' .. 'All languages and Windows localisations are supported automatically.\n\n' .. 'Technically-minded users may save the backup configuration as Windows batch ' .. 'files to\nincorporate into their own backup process (including fully automated backups).' local lblTitle = iup.label{title = Heading, alignment = 'ALEFT', padding = '10x20'} -- folder controls and options local lblF = iup.label{title = gblOptions.Path or '',expand = 'HORIZONTAL'} local btnF = iup.button{title = 'Select', padding = '10x3', tip = 'Destination folder where scripts and backup data are written.', action = function(self) SelectFolder(MainForm) UpdateMenu() end} local chkB = iup.toggle{title = 'Write to dated subfolder', expand = 'YES', active = 'NO', tip = 'Write backup to dated subfolder.'} local fraF = iup.frame{iup.vbox{lblF, chkB, btnF; alignment = 'ACENTER'}; title = 'Backup folder'} -- general controls and options (use similar containers for all frames for consistent appearance) local chkM1 = iup.toggle{title = 'Normal', expand = 'HORIZONTAL', tip = 'Run backup or restore immediately.'} local chkM2 = iup.toggle{title = 'Advanced', expand = 'HORIZONTAL', tip = 'Save the script files without running the backup or restore.'} local hboxM = iup.hbox{chkM1, chkM2} local radM = iup.radio{hboxM} local fraM = iup.frame{iup.hbox{radM}, title = 'Operating mode'} -- button display local btnB = iup.button{active = 'NO', action = function(self) Backup() MainForm.BringFront = 'YES' end} local btnR = iup.button{title = 'Restore Script', padding = '10x3', action = function(self) Restore(MainForm) MainForm.BringFront = 'YES' end} local btnH = iup.button{title = 'Help', tip = 'Display plugin help page.\n' .. '(hold down either Shift key to display\nSystem Information)', action = function(self) GetHelp(MainForm) end} local btnX = iup.button{title = 'Close', tip = 'Close plugin.', action = function(self) return iup.CLOSE end} local hButtons = iup.hbox{btnB, btnR, btnH, btnX; cgap = 20, cmargin = 'x15', normalizesize = 'BOTH'} -- enhanced tool tips btnB.TipBalloonTitleIcon = '1' btnF.TipBalloonTitle = 'Select Folder' btnF.TipBalloonTitleIcon = '1' chkB.TipBalloonTitle = 'Dated Subfolder' chkB.TipBalloonTitleIcon = '1' chkM1.TipBalloonTitle = 'Normal Mode' chkM1.TipBalloonTitleIcon = '2' chkM2.TipBalloonTitle = 'Advanced Mode' chkM2.TipBalloonTitleIcon = '1' btnH.TipBalloonTitle = 'Plugin Help' btnH.TipBalloonTitleIcon = '1' btnX.TipBalloonTitle = 'Close Plugin' btnX.TipBalloonTitleIcon = '3' -- menu update function function UpdateMenu() if gblOptions.Path then lblF.Title = gblOptions.Path btnB.Active = 'YES' chkB.Active = 'YES' end if gblOptions.Dated then chkB.Value = 'YES' else chkB.Value = 'NO' end if gblOptions.Advanced then chkM2.Value = 'ON' btnB.Title = 'Backup Script' btnR.Title = 'Restore Script' btnB.Tip = 'Save backup batch file to defined backup folder.' btnB.TipBalloonTitle = 'Save Backup Script' btnR.Tip = 'Save restore batch file to defined backup folder.' btnR.TipBalloonTitle = 'Save Restore Script' btnR.TipBalloonTitleIcon = '1' else chkM1.Value = 'ON' btnB.Title = 'Backup' btnB.Size = btnR.Size btnB.Tip = 'Run backup now using defined options.' btnB.TipBalloonTitle = 'Run Backup' btnR.Title = 'Restore' btnR.Size = btnB.Size btnR.Tip = 'Run restore now using defined options.' btnR.TipBalloonTitle = 'Run Restore' btnR.TipBalloonTitleIcon = '2' end end -- callback actions function chkM2:valuechanged_cb() gblOptions.Advanced = (chkM2.Value == 'ON') UpdateMenu() end function chkB:valuechanged_cb() gblOptions.Dated = (chkB.Value == 'ON') end -- assemble form local vForm = iup.vbox{lblTitle, fraM, fraF, hButtons; alignment = 'ACENTER', cgap = 5, cmargin = '10x3'} MainForm = iup.dialog{vForm; minbox='NO', maxbox='NO', title='Backup and Restore Family Historian Settings via Windows (3.0)'} MainForm:map() -- determine form layout prior to assigning button labels and resizing MainForm.tipballoon = 'YES' UpdateMenu() if fhGetAppVersion() > 6 then iup.SetAttribute(MainForm, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND')) elseif IsExtendedDesktop() then local msg = 'Versions of Family Historian prior to 7 cannot determine which monitor ' .. 'the application is running in, so the main plugin menu will always open in the ' .. 'primary monitor.' MessageBox(msg, 'OK', 'WARNING') end MainForm:popup() SaveOptions() end -- ********************************************************************* function SelectFolder(MainForm) local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path, title = 'Select folder for scripts and backup files', parentdialog = MainForm} filedlg:popup() if filedlg.Status == '-1' then return end local path = filedlg.Value gblOptions.Path = path gblOptions.ShortPath = FSO:GetFolder(path).ShortPath end -- ********************************************************************* function Backup() -- check for existing backup local BackupPath = gblOptions.Path if gblOptions.Dated then BackupPath = BackupPath .. '\\fhBackup_' .. os.date('%Y-%m-%d') end -- check all files and folders present if existing backup local i = 0 if FSO:FolderExists(BackupPath .. '\\fhProgramData') then i = i + 1 end if FSO:FolderExists(BackupPath .. '\\fhAppData') then i = i + 1 end if FSO:FileExists(BackupPath .. '\\fhCURRENT_USER.reg') then i = i + 1 end if FSO:FileExists(BackupPath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end if not gblOptions.Advanced then if i > 0 and i < 4 then local msg = 'Existing backup in ' .. BackupPath .. ' is incomplete.\n\nIgnore error and update?' if MessageBox(msg, 'OKCANCEL', 'WARNING', nil, 2) ~= 1 then return end elseif i == 4 then local stamp = FSO:GetFile(BackupPath .. '\\fhCURRENT_USER.reg').DateLastModified local msg = os.date('Update existing backup in ' .. BackupPath .. '?\n\nLast update: ' .. stamp) if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Update', 2) ~= 1 then return end end end local tblScript, BackupFile = GetBackupScript(BackupPath) -- write script to file local F = io.open(BackupFile, 'w') if not F then MessageBox('Cannot write backup script.', 'OK', 'ERROR', 'File Access Error') return end table.insert(tblScript, '') F:write(table.concat(tblScript, '\n')) F:close() -- confirm successful write if gblOptions.Advanced then local msg = 'Backup script written to file ' .. gblOptions.Path .. '\\fhBackup.bat.' MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation') return end -- implement backup if IsExtendedDesktop() then MessageBox('The backup will now run in the primary monitor.', 'OK', 'INFORMATION') end if not fhShellExecute(BackupFile) then MessageBox('Backup failed to run.', 'OK', 'ERROR', 'Script Error') end end -- ********************************************************************* function GetBackupScript(BackupPath) -- prepare backup script local AppName = fhGetContextInfo('CI_APP_DATA_FOLDER'):match('[%w%s]+$') local UserKey = 'HKCU\\SOFTWARE\\Calico Pie\\' .. AppName local GlobalKey local _, err = GetRegKey('HKLM\\SOFTWARE\\WOW6432Node\\Calico Pie\\' .. AppName .. '\\2.0\\Preferences\\Language') if not err then GlobalKey = 'HKLM\\SOFTWARE\\WOW6432Node\\Calico Pie\\' .. AppName .. '\\2.0\\Preferences' else GlobalKey = 'HKLM\\SOFTWARE\\Calico Pie\\' .. AppName .. '\\2.0\\Preferences' end local tblT = {} -- build the script in this table table.insert(tblT, '@echo off') table.insert(tblT, 'echo Backup started...') -- set path according to whether dated subfolder or not if not gblOptions.Dated then table.insert(tblT, 'set p=' .. gblOptions.ShortPath) else table.insert(tblT, 'set basepath=' .. gblOptions.ShortPath) local cmd = 'for /f %%D in ( \'PowerShell Get-Date -Format "_yyyy-MM-dd"\' ) do set suffix=%%D' table.insert(tblT, cmd) table.insert(tblT, 'set p=%basepath%\\fhBackup%suffix%') end -- define command script table.insert(tblT, 'robocopy "' .. gblOptions.ProgramData .. '" "%p%\\fhProgramData" /MIR /XD "' .. fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Map\\Cache" /NJH') table.insert(tblT, 'robocopy "' .. gblOptions.AppDataPath .. '" "%p%\\fhAppData" /MIR /NJH') table.insert(tblT, 'reg export "' .. UserKey .. '" "%p%\\fhCURRENT_USER.reg" /y') table.insert(tblT, 'reg export "' .. GlobalKey .. '" "%p%\\fhLOCAL_MACHINE.reg" /y') -- determine file used to save command script and add self-delete command if immediate backup local BackupFile if gblOptions.Advanced then BackupFile = gblOptions.ShortPath .. '\\fhBackup.bat' else BackupFile = gblOptions.Temp .. '\\~fhBackup.bat' table.insert(tblT, 'pause') table.insert(tblT, 'del "' .. BackupFile .. '"') end return tblT, BackupFile end -- ********************************************************************* function Restore(MainForm) -- check for Advanced mode local RestoreFolder, ShortFolder, RestoreFile, ShortFile if gblOptions.Advanced then RestoreFolder = gblOptions.Path RestoreFile = gblOptions.Path .. '\\fhRestore.bat' ShortFolder = gblOptions.ShortPath ShortFile = gblOptions.ShortPath .. '\\fhRestore.bat' else RestoreFolder, ShortFolder = SelectRestoreFolder(MainForm) if not RestoreFolder then return end RestoreFile = gblOptions.Temp .. '\\~fhRestore.bat' ShortFile = gblOptions.Temp .. '\\~fhRestore.bat' end local tblScript = GetRestoreScript(RestoreFolder, ShortFolder) -- write script to file (path cannot be Unicode, so simple method ok) local F = io.open(ShortFile, 'w') if not F then MessageBox('Cannot write restore script.', 'OK', 'ERROR', 'File Access Error') return end table.insert(tblScript, '') F:write(table.concat(tblScript, '\n')) F:close() -- confirm successful write if gblOptions.Advanced then local msg = 'Restore script written to file ' .. RestoreFile .. '.' MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation') return end -- remove default files and folders from Registry file before restoring RemoveDefaults(RestoreFolder) -- warn the user what is about to happen and request approval local msg = 'Family Historian will now be closed in order to complete the restore operation.\n\n' .. 'Are you sure that you want to do this?' if MessageBox(msg, 'YESNO', 'WARNING', 'Confirm Automatic Restore - CAUTION', 2) ~= 1 then return end -- generate auto-close script local CloseFile = gblOptions.Temp .. '\\' .. '~fhAutoClose.bat' local tblT = {} table.insert(tblT, '@echo off') table.insert(tblT, 'echo Restore started...') table.insert(tblT, 'powershell start-sleep -s 1') table.insert(tblT, 'cls') table.insert(tblT, 'taskkill /IM fh.exe /f') table.insert(tblT, 'powershell start-sleep -s 1') table.insert(tblT, 'call "' .. RestoreFile .. '"') table.insert(tblT, 'del "' .. RestoreFile .. '"') table.insert(tblT, 'del "' .. CloseFile .. '"') table.insert(tblT, '') local F = io.open(CloseFile, 'w') if not F then MessageBox('Cannot write auto-close script.', 'OK', 'ERROR', 'File Access Error') return end F:write(table.concat(tblT, '\n')) F:close() -- check that only one instance of FH is running if CountInstances() > 1 then local msg = 'There may be more than one instance of Family Historian open.\n\n' .. 'Proceeding with the restore operation could cause data loss. Please close ' .. 'any additional copies and re-run the plugin.' MessageBox(msg, 'OK', 'ERROR', 'Multiple Instances Detected') os.remove(RestoreFile) os.remove(CloseFile) return end -- execute the kamikaze close and bale out... fhShellExecute(CloseFile) end -- ********************************************************************* function SelectRestoreFolder(MainForm) -- select backup to restore local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path, parentdialog = MainForm, title = 'Select folder that contains the backup you wish to restore'} filedlg:popup() if filedlg.Status == '-1' then return end local RestorePath = filedlg.Value local ShortPath = FSO:GetFolder(RestorePath).ShortPath -- check folder contains an identifiable backup local BackupVersion, B1, B2, B3 -- is there a valid new format backup in this folder? local i = 0 if FSO:FolderExists(RestorePath .. '\\fhProgramData') then i = i + 1 end if FSO:FolderExists(RestorePath .. '\\fhAppData') then i = i + 1 end if FSO:FileExists(RestorePath .. '\\fhCURRENT_USER.reg') then i = i + 1 end if FSO:FileExists(RestorePath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end if i == 4 then local KeyFile = RestorePath .. '\\fhProgramData\\Plugin Data\\' .. 'Backup and Restore Family Historian Settings via Windows.ini' ShortKey = FSO:GetFile(KeyFile).ShortPath for Line in io.lines(ShortKey) do local k, v = Line:match('^(%w+)=([%w%p%s]+)$') if k == 'Version' then BackupVersion = v B1, B2, B3 = BackupVersion:match('(%d+).(%d+).(%d+)') end end end if not (B1 and B2 and B3) then local msg = 'Selected folder does not appear to contain a backup.' MessageBox(msg, 'OK', 'ERROR', 'Folder Selection Error') return end -- check backup version matches local V1, V2, V3 = fhGetAppVersion() B1, B2, B3 = tonumber(B1), tonumber(B2), tonumber(B3) if V1 ~= B1 then local msg = 'Cannot restore a Version ' .. B1 .. ' backup into Version ' .. V1 .. '.' MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility') return elseif B2 > V2 or (B2 == V2 and B3 > V3) then local msg = 'Cannot restore a Version ' .. BackupVersion .. ' backup into Version ' .. gblOptions.Version .. '.\n\n' .. 'You should update your present copy of Family Historian before restoring the backup.' MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility') return elseif B2 < V2 or (B2 == V2 and B3 < V3) then local msg = 'Restoring a Version ' .. BackupVersion .. ' backup into Version ' .. gblOptions.Version .. ' is potentially risky.\n\nAre you sure that you want to do this?' if MessageBox(msg, 'YESNO', 'WARNING', 'Potential Version Conflict', 2) ~= 1 then return end end -- get backup creation date and time local stamp = FSO:GetFile(RestorePath .. '\\fhCURRENT_USER.reg').DateLastModified local msg = 'Backup in ' .. RestorePath .. ' was created ' .. stamp .. '. Is this the correct backup?' if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Backup Selection', 2) ~= 1 then return end return RestorePath, ShortPath end -- ********************************************************************* function GetRestoreScript(RestoreFolder, ShortFolder) local tblT = {} -- build the script in this table table.insert(tblT, '@echo off') local Switch = '/MIR /NJH' table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhProgramData" "' .. gblOptions.ProgramData .. '" ' .. Switch) table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhAppData" "' .. gblOptions.AppDataPath .. '" ' .. Switch) table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhCURRENT_USER.reg"') table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhLOCAL_MACHINE.reg"') if not gblOptions.Advanced then table.insert(tblT, 'pause') end return tblT end -- ********************************************************************* function RemoveDefaults(RestoreFolder) -- remove defaults that may not be valid in new installation local FileName = RestoreFolder .. '\\fhLOCAL_MACHINE.reg' if not FSO:FileExists(FileName) then MessageBox('Restore folder is missing a required Registry file.', 'OK', 'ERROR') return end local TextStream = FSO:OpenTextFile(FileName, 1, false, -1) local Data = TextStream:ReadAll() TextStream:Close() local tblT = {} Data = Data:gsub('\r\n\r\n', '\r\n \r\n') -- simple workaround to include blank lines for Line in Data:gmatch('[^\r\n]+') do if Line == ' ' then Line = '' end local T1 = Line:match('^"Backup Directory"=') local T2 = Line:match('^"Default File"=') local T3 = Line:match('^"FH Root Folder"=') local T4 = Line:match('^"Emulator%-Compat%-Mode"=') if not (T1 or T2 or T3 or T4) then table.insert(tblT, Line) end end table.insert(tblT, '') TextStream = FSO:OpenTextFile(FileName, 2, false, -1) TextStream:Write(table.concat(tblT, '\n')) TextStream:Close() end -- ********************************************************************* function GetOptions() gblOptions = {} local tblO = {} -- get system level options gblOptions.Temp = os.getenv('TEMP') gblOptions.ProgramData = fhGetContextInfo('CI_APP_DATA_FOLDER') local AppName = fhGetContextInfo('CI_APP_DATA_FOLDER'):match('[%w%s]+$') gblOptions.AppData = os.getenv('TEMP'):match('^(.+)Local\\Temp$') .. 'Roaming\\Calico Pie\\' .. AppName gblOptions.AppDataPath = FSO:GetFolder(gblOptions.AppData).ShortPath -- get saved options local File = gblOptions.ProgramData .. '\\Plugin Data\\' .. 'Backup and Restore Family Historian Settings via Windows.ini' if FSO:FileExists(File) then for line in io.lines(File) do local i = line:find('=') if i then tblO[line:sub(1, i-1)] = line:sub(i+1) end end if tblO.Path and FSO:FolderExists(tblO.Path) then gblOptions.Path = tblO.Path gblOptions.ShortPath = FSO:GetFolder(gblOptions.Path).ShortPath end if tblO.Advanced == '1' then gblOptions.Advanced = true end if tblO.Dated == '1' then gblOptions.Dated = true end end -- set version option and save back to file to ensure saved on backup local V1, V2, V3 = fhGetAppVersion() gblOptions.Version = V1 .. '.' .. V2 .. '.' .. V3 SaveOptions() return true end -- ********************************************************************* function SaveOptions() -- save options to file local OptionsFolder = gblOptions.ProgramData .. '\\Plugin Data' if not FSO:FolderExists(OptionsFolder) then FSO:CreateFolder(OptionsFolder) end local OptionsFile = OptionsFolder .. '\\Backup and Restore Family Historian Settings via Windows.ini' local F = io.open(OptionsFile, 'w') if not F then MessageBox('Cannot open options file.', 'OK', 'ERROR', 'File Access Error') return end F:write('[Options]\n') F:write('Version=' .. gblOptions.Version .. '\n') if gblOptions.Path then F:write('Path=' .. gblOptions.Path .. '\n') end if gblOptions.Advanced then F:write('Advanced=1\n') end if gblOptions.Dated then F:write('Dated=1\n') end F:close() end -- ********************************************************************* function GetHelp(MainForm) if iup.GetGlobal('SHIFTKEY') == 'OFF' then fhShellExecute('https://pluginstore.family-historian.co.uk/page/help/' .. 'backup-and-restore-family-historian-settings-via-windows') end -- hidden diagnostic window gblOptions.CPU = GetRegKey('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\' .. 'Environment\\PROCESSOR_ARCHITECTURE') local tblT = { 'Processor Architecture = ' .. gblOptions.CPU or 'n/a', 'User Name (IUP) = ' .. (iup.GetGlobal('USERNAME') or ''), 'Temp Folder = ' .. gblOptions.Temp, 'ProgramData Folder = ' .. gblOptions.ProgramData:gsub(' ', ' '), 'AppData Folder = ' .. gblOptions.AppData:gsub(' ', ' ') } MessageBox(table.concat(tblT, '\n'), 'OK', 'INFORMATION', 'System Information') end -- ********************************************************************* function IsExtendedDesktop() -- determines if system has an extended desktop local xd = iup.GetGlobal('VIRTUALSCREEN') local x, y, width, height = xd:match('^(%S+)%s(%S+)%s(%S+)%s(%S+)$') local fs = iup.GetGlobal('FULLSIZE') local ExtendedDesktop = fs ~= width .. 'x' .. height return ExtendedDesktop end -- ********************************************************************* function CountInstances() -- checks just a single instance of FH is running local F = io.popen('powershell get-process *fh*') if not F then local msg = 'Cannot determine number of Family Historian copies running.' MessageBox(msg, 'OK', 'ERROR', 'File Handle Access Error') return end local S = F:read('*a') F:close() local Instances = 0 for line in S:gmatch('[^\r\n]+') do if line:lower():match('%sfh%s+$') then Instances = Instances + 1 end end return Instances end -- ********************************************************************* function GetRegKey(key) local sh = luacom.CreateObject 'WScript.Shell' local ans if pcall(function () ans = sh:RegRead(key) end) then return ans else return nil,true end end -- ********************************************************************* function MessageBox(Message, Buttons, Icon, Title, Default) -- replaces built-in function with custom version containing more options local msgdlg = iup.messagedlg{value = Message, buttons = Buttons, dialogtype = Icon, title = Title or fhGetContextInfo('CI_PLUGIN_NAME'), buttondefault = Default, parentdialog = MainForm} msgdlg:popup() return tonumber(msgdlg.ButtonResponse) end -- ********************************************************************* main()