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.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()

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