Plugins and Language Packs for Family Historian

Backup and Restore Family Historian Settings via Windows.fh_lua

--[[
@Title:       Backup and Restore Family Historian Settings via Windows
@Type:        Standard
@Author:      Mark Draper
@Version:     3.1
@LastUpdated: 26 Oct 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, giving much faster and more convenient
              operation. It also provides the option to implement more advanced features such as
              single-click rapid backup or automated backup that are not available in either the legacy
              settings backup plugin or the new native settings backup available in version 8 of Family
              Historian.
]]

--[[
	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
	Version 3.1 (Oct 2026)
		Modified run command to call cmd specifically to be robust to default app change
		Simplified script to limit references to Short Paths
		Backup location must be ANSI, for simpler coding and avoiding Windows script issues with Unicode
		Significant general code tidying
]]

fhInitialise(5,0,0,'save_required')

require('luacom')
require('iuplua')

if fhGetAppVersion() > 6 then
	fh = require('fhUtils')
	fh.setIupDefaults()
else
	iup.SetGlobal('CUSTOMQUITMESSAGE','YES')
	iup.SetGlobal('UTF8MODE','YES')
end

gblAppPath = 'Calico Pie\\Family Historian'
if fhGetAppVersion() > 7 then gblAppPath = gblAppPath .. ' ' .. fhGetAppVersion() end
gblOptions = {}

FSO = luacom.CreateObject('Scripting.FileSystemObject')

-- *********************************************************************

function main()

	if IsWINE() then return end				-- exit with message if emulator detected

	if not GetOptions() then return end					-- get plugin options

	local MainForm

	-- 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.', 
			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'}

	-- 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.'
			btnR.Tip = 'Save restore batch file to defined backup folder.'
		else
			chkM1.Value = 'ON'
			btnB.Title = 'Backup'
			btnB.Size = btnR.Size
			btnB.Tip = 'Run backup now using defined options.'
			btnR.Title = 'Restore'
			btnR.Size = btnB.Size
			btnR.Tip = 'Run restore now using defined options.'
		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', resize = 'NO',
			title='Backup and Restore Family Historian Settings via Windows (3.1)'}
	MainForm:map()		-- determine form layout prior to assigning button labels and resizing

	UpdateMenu()

	if fhGetAppVersion() > 6 then
		iup.SetAttribute(MainForm, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
	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

	fhSetConversionLossFlag(false)
	local _ = fhConvertUTF8toANSI(filedlg.Value)
	if fhIsConversionLossFlagSet() then
		local msg = 'To avoid potential Windows issues with international character sets, selected ' ..
				'folder name should contain Latin characters only. Please select an alternative location.'
		MessageBox(msg, 'OK', 'WARNING', 'Folder name incompatibility')
		fhSetConversionLossFlag(false)
	else
		gblOptions.Path = filedlg.Value
	end
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

	if not SaveTextFile(BackupFile, table.concat(tblScript, '\n')) then
		MessageBox('Cannot write backup script.', 'OK', 'ERROR', 'File Access Error')
		return
	end

	-- 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(os.getenv('ComSpec'), '/C ' .. BackupFile) then
		MessageBox('Backup failed to run.', 'OK', 'ERROR', 'Script Error')
	end
end

-- *********************************************************************

function GetBackupScript(BackupPath)

	-- prepare backup script

 	local GlobalKey
	if fhGetAppVersion() < 8 then
		GlobalKey = 'HKLM\\SOFTWARE\\WOW6432Node\\' .. gblAppPath .. '\\2.0\\Preferences'
	else
		GlobalKey = 'HKLM\\SOFTWARE\\' .. gblAppPath .. '\\2.0\\Preferences'
	end
	local UserKey = 'HKCU\\SOFTWARE\\' .. gblAppPath

	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.Path)
	else
		table.insert(tblT, 'set basepath=' .. gblOptions.Path)
		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.AppData .. '" "%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')
	table.insert(tblT, '')

	-- determine file used to save command script and add self-delete command if immediate backup

	local BackupFile
	if gblOptions.Advanced then
		BackupFile = gblOptions.Path .. '\\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, RestoreFile
	if gblOptions.Advanced then
		RestoreFolder = gblOptions.Path
		RestoreFile = gblOptions.Path .. '\\fhRestore.bat'
	else
		RestoreFolder = SelectRestoreFolder(MainForm)
		if not RestoreFolder then return end
		RestoreFile = gblOptions.Temp .. '\\~fhRestore.bat'
	end

	local tblScript = GetRestoreScript(RestoreFolder)

	-- write script to file

	if not SaveTextFile(RestoreFile, table.concat(tblScript, '\n')) then
		MessageBox('Cannot write restore script.', 'OK', 'ERROR', 'File Access Error')
		return
	end

	-- 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, '')

	if not SaveTextFile(CloseFile, table.concat(tblT, '\n')) then
		MessageBox('Cannot write auto-close script.', 'OK', 'ERROR', 'File Access Error')
		return
	end

	-- 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

	-- check folder contains an identifiable backup
	
	local BackupVersion, B1, B2, B3

	-- is there a valid new format backup in this folder?

	if FSO:FolderExists(RestorePath .. '\\fhProgramData') and
			FSO:FolderExists(RestorePath .. '\\fhAppData') and
			FSO:FileExists(RestorePath .. '\\fhCURRENT_USER.reg') and
			FSO:FileExists(RestorePath .. '\\fhLOCAL_MACHINE.reg') then
		local OptionsFile = RestorePath .. '\\fhProgramData\\Plugin Data\\' ..
				'Backup and Restore Family Historian Settings via Windows.ini'
		local TextStream = FSO:OpenTextFile(OptionsFile, 1, false, 0)
		while not TextStream.AtEndOfStream do
			local Line = TextStream:ReadLine()
			B1, B2, B3 = Line:match('Version=(%d+).(%d+).(%d+)')
			if B1 and B2 and B3 then
				BackupVersion = B1 .. '.' .. B2 .. '.' .. B3
				break
			 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
end

-- *********************************************************************

function GetRestoreScript(RestoreFolder)

	local tblT = {}												-- build the script in this table
	table.insert(tblT, '@echo off')
	local Switch = '/MIR /NJH'
	table.insert(tblT, 'robocopy "' .. RestoreFolder .. '\\fhProgramData" "' .. 
			gblOptions.ProgramData .. '" ' .. Switch)
	table.insert(tblT, 'robocopy "' .. RestoreFolder .. '\\fhAppData" "' .. 
			gblOptions.AppData .. '" ' .. Switch)
	table.insert(tblT, 'reg import "' .. RestoreFolder .. '\\fhCURRENT_USER.reg"')
	table.insert(tblT, 'reg import "' .. RestoreFolder .. '\\fhLOCAL_MACHINE.reg"')
	if not gblOptions.Advanced then
		table.insert(tblT, 'pause')
	end
	table.insert(tblT, '')
	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 tblT = {}
	local TextStream = FSO:OpenTextFile(FileName, 1, false, -1)
	while not TextStream.AtEndOfStream do
		local Line = TextStream:ReadLine()
		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:Close()
	SaveTextFile(FileName, table.concat(tblT, '\n'))
end

-- *********************************************************************

function GetOptions()

	-- get system level options

	gblOptions.ProgramData = fhGetContextInfo('CI_APP_DATA_FOLDER')
	local Temp = os.getenv('TEMP')												-- always ASCII
	gblOptions.Temp = Temp
	gblOptions.AppData = Temp:gsub('Local\\Temp', 'Roaming\\') .. gblAppPath

	print('Temp = ' .. (gblOptions.Temp or ''))
	print('ProgramData = ' .. (gblOptions.ProgramData or ''))
	print('AppData = ' .. (gblOptions.AppData or ''))

	-- get saved options

	local File = gblOptions.ProgramData .. '\\Plugin Data\\' ..
			'Backup and Restore Family Historian Settings via Windows.ini'
	if FSO:FileExists(File) then
		local TextStream = FSO:OpenTextFile(File, 1)
		while not TextStream.AtEndOfStream do
			local Line = TextStream:ReadLine()
			if Line:match('^Path=') then gblOptions.Path = Line:sub(6)
			elseif Line == 'Dated=1' then gblOptions.Dated = true
			elseif Line == 'Advanced=1' then gblOptions.Advanced = true end
		end
		TextStream:Close()

		if not FSO:FolderExists(gblOptions.Path) then
			gblOptions.Path = nil
		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 tblO = {}
	table.insert(tblO, '[Options]')
	if gblOptions.Version then table.insert(tblO, 'Version=' .. gblOptions.Version) end
	if gblOptions.Path then table.insert(tblO, 'Path=' .. gblOptions.Path) end
	if gblOptions.Advanced then table.insert(tblO, 'Advanced=1') end
	if gblOptions.Dated then table.insert(tblO, 'Dated=1') end
	table.insert(tblO, '')
	SaveTextFile(OptionsFile, table.concat(tblO, '\n'))
end

-- *********************************************************************

function GetHelp(MainForm)

	fhShellExecute('https://pluginstore.family-historian.co.uk/page/help/' ..
			'backup-and-restore-family-historian-settings-via-windows')
end

-- *********************************************************************

function SaveTextFile(Path, Contents) 

	-- saves file with no BOM

	local F = io.open(Path, 'w')
	if F then
		F:write(Contents)
		F:close()
		return true
	end
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 IsWINE()

	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\n'
		if fhGetAppVersion() < 8 then
			msg = msg .. 'Please upgrade to Family Historian version 8, which has a built-in ' ..
					'settings backup if you wish to save your customisations and settings from ' ..
					'within Family Historian.'
		else
			msg = msg .. 'You are recommended to use the new settings backup option built into ' ..
					'version 8 of Family Historian.'
		end
		MessageBox(msg, 'OK', 'ERROR', 'Emulator Compatibility Warning')
		return 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()

Source: Backup-and-Restore-Family-Historian-Settings-via-Windows-10.fh_lua