Link Lister.fh_lua--[[
@Title: Link Lister
@Type: standard
@Author: Helen Wright
@Contributors:
@Version: 1.0
@LastUpdated: 7th August 2025
@Licence: This plugin is copyright (c) 2025 Helen Wright & contributors and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: This plugin will find all records that are linked From or To the currently selected record, or the Property Box record if there is no selected record. It displays the From Record, the Field that contains the link (with its parent field and date if any for context), and the To Record.
]]
--[[ChangeLog:
Version 1.0: Initial release
]]
--------------------------------------------------------------
--FH VERSION CHECK
--------------------------------------------------------------
-- Ensure this plugin runs on Family Historian version 7 or higher
-- It might run on earlier versions but I'm not in a position to test or support them.
if fhGetAppVersion() < 7 then
fhMessageBox("This plugin requires FH7 and will exit.")
return
end
-- Set string encoding to UTF-8 for proper handling of international characters
fhSetStringEncoding("UTF-8")
--------------------------------------------------------------
--KEY VARIABLES
--------------------------------------------------------------
local ptrFocus -- for selected or property box record - the record we're analyzing for links
local myResults --for result display - stores the Results object for managing output
-----------------------------------------------------------------
--HELPER FUNCTIONS AND LIBRARIES
-----------------------------------------------------------------
---Results class for managing the results display within an FH plugin
---@param intTableCount integer Number of columns in the results table
---@return table Results object with methods for managing result display
local function Results(intTableCount)
---Local shallow copy helper - creates a copy of a table without deep copying nested structures
---This is used to safely copy configuration arrays without creating references
---@param tbl table Table to copy
---@return table Shallow copy of the input table
local function shallow_copy(tbl)
local t = {}
for k, v in pairs(tbl) do
t[k] = v
end
return t
end
--public methods and associated private state variables
local iRes = 0 -- index used to track results - counts how many result rows we have
local strTitle = "" -- stores the title for the results window
local strNoResults = "" -- stores the message to display when there are no results
local tblResults = {} --table of results tables - stores all the data for each column
local tblVisibility = {} -- controls which columns are visible in the results
local tblSort = {} -- defines the sort order for each column
local tblResultHeadings = {} -- stores the column headers
local tblResultType = {} -- defines the data type for each column (text, integer, item, etc.)
tblResultWidth = {} -- defines the width for each column
-- Initialize the results tables - each table will hold one column of data
-- This creates separate arrays for each column to store the data efficiently
for i = 1, intTableCount do
tblResults[i] = {}
end
---Update function: adds a new row of results to the display
---tblNewResults should contain one value for each column
---This is the main method for adding data to the results
---@param tblNewResults table Array of values for the new row
local Update = function(tblNewResults)
iRes = iRes + 1 -- increment the result counter
for i, v in ipairs(tblNewResults) do
tblResults[i][iRes] = v -- store each value in its appropriate column
end
end
---Title function: sets the title for the results window
---This will be displayed at the top of the results window
---@param str string Title for the results window
local Title = function(str)
strTitle = str
end
---NoResults function: sets the message to display when there are no results
---This provides user feedback when no links are found
---@param str string Message to display when there are no results
local NoResults = function(str)
strNoResults = str
end
---Types function: defines the data type for each column
---Types can be: "text", "integer", "item", "date", etc.
---This affects how FH displays and sorts the data
---@param types table Array of data types for each column
local Types = function(types)
tblResultType = shallow_copy(types)
end
---Headings function: sets the column headers
---These are the labels that appear at the top of each column
---@param headings table Array of column header strings
local Headings = function(headings)
tblResultHeadings = shallow_copy(headings)
end
---Visibility function: controls which columns are shown
---Values can be "show" or "hide"
---"buddy" makes a column invisible but keeps it for sorting purposes
---@param visibility table Array of visibility settings for each column
local Visibility = function(visibility)
tblVisibility = shallow_copy(visibility)
end
---Sort function: defines the sort order for each column
---Lower numbers = higher priority in sorting
---This determines the default sort order when results are displayed
---@param sort table Array of sort priorities for each column
local Sort = function(sort)
tblSort = shallow_copy(sort)
end
---Width function: defines the width for each column
---@param width table Array of widths for each column
local Width = function(width)
tblResultWidth = shallow_copy(width)
end
---Display function: outputs all collected results to Family Historian's result window
---This is the final step that shows all the collected data to the user
local Display = function()
if iRes > 0 then -- there are results to display
-- Set the window title
fhOutputResultSetTitles(strTitle)
-- Output each column with its configuration
-- This creates the actual result set in FH's display
for i, _ in ipairs(tblResults) do
fhOutputResultSetColumn(
tblResultHeadings[i], -- Column header
tblResultType[i], -- Data type
tblResults[i], -- The data for this column
iRes, -- Number of rows
tblResultWidth[i] or 80, -- Column width or 80 if not set
"align_left", -- Text alignment
tblSort[i], -- Sort priority
true, -- Sortable
"default", -- Sort direction
tblVisibility[i] -- Visibility setting
)
end
-- Update the display to show the results
fhUpdateDisplay()
else
-- No results found - show informational message
fhMessageBox(strNoResults, "MB_OK", "MB_ICONINFORMATION")
end
end
--expose public methods - return an object with all the public functions
-- This creates the public interface for the Results class
return {
Title = Title,
Headings = Headings,
Visibility = Visibility,
Types = Types,
Update = Update,
Display = Display,
Sort = Sort,
NoResults = NoResults,
Width = Width,
}
end
---getAllRecords function: Retrieves all records in the FH database except the focus record
---This creates a comprehensive list of all records to check for links
---@return table Array of ItemPointer objects representing all records
local function getAllRecords()
local recs = {}
---allrecords function: Creates an iterator for all records of a specific type
---This is a helper function that efficiently iterates through records
---@param type string The record type to iterate through
---@return function Iterator function that returns ItemPointer objects
local function allrecords(type)
local pi = fhNewItemPtr() -- Primary pointer for iteration
local p2 = fhNewItemPtr() -- Secondary pointer for returning current record
pi:MoveToFirstRecord(type) -- Start at the first record of this type
return function()
p2:MoveTo(pi) -- Copy current position to return pointer
pi:MoveNext() -- Move to next record
if p2:IsNotNull() then
return p2 -- Return the current record
end
end
end
-- Get the total number of record types in the database
local recordTypeCount = fhGetRecordTypeCount()
-- Iterate through all record types
for i = 0, recordTypeCount - 1 do
local recordType = fhGetRecordTypeTag(i) -- Get the tag for this record type
-- Iterate through all records of this type
for ptr in allrecords(recordType) do
-- Only include records that are not the same as our focus record
-- This prevents self-referencing and reduces unnecessary processing
if ptr:IsNotNull() and not ptr:IsSame(ptrFocus) then
table.insert(recs, ptr:Clone()) -- Add a copy to our list
end
end
end
return recs
end
-------------------------------------
--GET LINKED RECORDS
-------------------------------------
---GetLinkedRecords function: Gets all records and fields that are linked to the focus record, both Incoming and Outgoing, and updates myResults
---This is the core function that finds all links to and from the focus record
---@param ptrFocus ItemPointer The item to get linked records for
local function getLinkedRecords(ptrFocus)
-- Get all records in the database (except the focus record)
local tblCheck = getAllRecords()
---Recursive function to check all fields in a record for links
---@param direction string The direction of the link (From or To)
---@param record ItemPointer The record to check
---@param target ItemPointer The target record to find links to
---@param source ItemPointer The source record (for results display)
local function checkFieldsForLinks(direction, record, target, source)
local ptrField = fhNewItemPtr()
ptrField:MoveToFirstChildItem(record) -- Start with the first field
-- Iterate through all fields in this record
while ptrField:IsNotNull() do
-- Check if this field contains a link to our target record
local linkValue = fhGetValueAsLink(ptrField)
if linkValue and linkValue:IsNotNull() and linkValue:IsSame(target) then
-- Found a link! Add it to our results
local ptrParent = fhNewItemPtr()
ptrParent:MoveToParentItem(ptrField)
local strField = fhGetDisplayText(ptrField)
local ptrRole = fhGetItemPtr(ptrField, "~.ROLE") --only present for _SHAR or ASSO tags
if ptrRole:IsNotNull() then
strField = strField .. " (" .. fhGetDisplayText(ptrRole) .. ")"
end --special handling for witnesses
local ptrDate = fhGetItemPtr(ptrParent, "~.DATE")
local dpDate = fhNewDatePt()
if ptrDate:IsNotNull() then
local dtDate = fhGetValueAsDate(ptrDate)
dpDate = dtDate:GetDatePt1()
end
myResults.Update({
direction, -- "From" or "To" indicating link direction
source:Clone(), -- The record containing this field
fhGetRecordId(source), -- The record ID of the source
fhGetDisplayText(ptrParent), -- The parent field description
ptrParent:Clone(), --The parent field itself
dpDate:Clone(), --The date of the item
strField, -- The field name/label
ptrField:Clone(), -- The field itself
target:Clone(), -- The target record being linked to
fhGetRecordId(target), -- The record ID of the target
})
end
-- Recursively check child fields
checkFieldsForLinks(direction, ptrField, target, source)
ptrField:MoveNext() -- Move to next field
end
end
--Check each record for links to/from the focus record
for _, record in ipairs(tblCheck) do
--Check Outgoing Links (records that ptrFocus links to)
-- This finds records that the focus record points to
if fhCallBuiltInFunction("LinksFrom", ptrFocus, record, "EXT") > 0 then
-- Find the specific fields in ptrFocus that link to this record
checkFieldsForLinks("From", ptrFocus, record, ptrFocus)
end
--Check Incoming Links (records that link to ptrFocus)
-- This finds records that point to the focus record
if fhCallBuiltInFunction("LinksFrom", record, ptrFocus, "EXT") > 0 then
-- Find the specific fields in this record that link to ptrFocus
checkFieldsForLinks("To", record, ptrFocus, record)
end
end
end
-------------------------------------
--MAIN
-------------------------------------
-- Get the currently selected record or the Property Box record, to be analysed for links
local ptrRecs = fhGetCurrentRecordSel()
if #ptrRecs == 0 or ptrRecs[1]:IsNull() then
-- No record selected - check for a property box record
ptrFocus = fhGetCurrentPropertyBoxRecord()
if ptrFocus:IsNull() then
-- No record available to analyze
fhMessageBox("No selected record or property box record found", "MB_OK", "MB_ICONSTOP")
return
end
elseif #ptrRecs > 1 then
fhMessageBox("Multiple records selected - please select only one", "MB_OK", "MB_ICONSTOP")
return
else
-- Use the first selected record
ptrFocus = ptrRecs[1]
end
-- Configure the result set for displaying our findings
myResults = Results(10) -- Initialize results handling with 9 columns
myResults.Title("Link Lister: " .. fhGetDisplayText(ptrFocus)) -- Set window title with record name
myResults.NoResults("No links found") -- Message when no links are discovered
myResults.Headings({ "Direction", "From", "From ID", "Context", "", "Date", "Field", "", "To", "To ID" }) -- Column headers
myResults.Types({ "text", "item", "integer", "text", "item", "date-point", "text", "item", "item", "integer" }) -- Data types for each column
myResults.Visibility({ "hide", "show", "show", "show", "buddy", "show", "show", "buddy", "show", "show" }) -- Which columns to show
myResults.Width({ 0, 180, 25, 180, 0, 80, 180, 0, 180, 25 }) -- Width for each column
myResults.Sort({ 1, 2, 3, 7, 0, 8, 6, 0, 9, 10 }) -- Sort by Direction, then From Record and Field, then Context and Date, and finally To Record
-- Process the selected record to find incoming and outgoing links
getLinkedRecords(ptrFocus)
-- Display all the results we found
myResults.Display()
--[[
@Title: Link Lister
@Type: standard
@Author: Helen Wright
@Contributors:
@Version: 1.0
@LastUpdated: 7th August 2025
@Licence: This plugin is copyright (c) 2025 Helen Wright & contributors and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: This plugin will find all records that are linked From or To the currently selected record, or the Property Box record if there is no selected record. It displays the From Record, the Field that contains the link (with its parent field and date if any for context), and the To Record.
]]
--[[ChangeLog:
Version 1.0: Initial release
]]
--------------------------------------------------------------
--FH VERSION CHECK
--------------------------------------------------------------
-- Ensure this plugin runs on Family Historian version 7 or higher
-- It might run on earlier versions but I'm not in a position to test or support them.
if fhGetAppVersion() < 7 then
fhMessageBox("This plugin requires FH7 and will exit.")
return
end
-- Set string encoding to UTF-8 for proper handling of international characters
fhSetStringEncoding("UTF-8")
--------------------------------------------------------------
--KEY VARIABLES
--------------------------------------------------------------
local ptrFocus -- for selected or property box record - the record we're analyzing for links
local myResults --for result display - stores the Results object for managing output
-----------------------------------------------------------------
--HELPER FUNCTIONS AND LIBRARIES
-----------------------------------------------------------------
---Results class for managing the results display within an FH plugin
---@param intTableCount integer Number of columns in the results table
---@return table Results object with methods for managing result display
local function Results(intTableCount)
---Local shallow copy helper - creates a copy of a table without deep copying nested structures
---This is used to safely copy configuration arrays without creating references
---@param tbl table Table to copy
---@return table Shallow copy of the input table
local function shallow_copy(tbl)
local t = {}
for k, v in pairs(tbl) do
t[k] = v
end
return t
end
--public methods and associated private state variables
local iRes = 0 -- index used to track results - counts how many result rows we have
local strTitle = "" -- stores the title for the results window
local strNoResults = "" -- stores the message to display when there are no results
local tblResults = {} --table of results tables - stores all the data for each column
local tblVisibility = {} -- controls which columns are visible in the results
local tblSort = {} -- defines the sort order for each column
local tblResultHeadings = {} -- stores the column headers
local tblResultType = {} -- defines the data type for each column (text, integer, item, etc.)
tblResultWidth = {} -- defines the width for each column
-- Initialize the results tables - each table will hold one column of data
-- This creates separate arrays for each column to store the data efficiently
for i = 1, intTableCount do
tblResults[i] = {}
end
---Update function: adds a new row of results to the display
---tblNewResults should contain one value for each column
---This is the main method for adding data to the results
---@param tblNewResults table Array of values for the new row
local Update = function(tblNewResults)
iRes = iRes + 1 -- increment the result counter
for i, v in ipairs(tblNewResults) do
tblResults[i][iRes] = v -- store each value in its appropriate column
end
end
---Title function: sets the title for the results window
---This will be displayed at the top of the results window
---@param str string Title for the results window
local Title = function(str)
strTitle = str
end
---NoResults function: sets the message to display when there are no results
---This provides user feedback when no links are found
---@param str string Message to display when there are no results
local NoResults = function(str)
strNoResults = str
end
---Types function: defines the data type for each column
---Types can be: "text", "integer", "item", "date", etc.
---This affects how FH displays and sorts the data
---@param types table Array of data types for each column
local Types = function(types)
tblResultType = shallow_copy(types)
end
---Headings function: sets the column headers
---These are the labels that appear at the top of each column
---@param headings table Array of column header strings
local Headings = function(headings)
tblResultHeadings = shallow_copy(headings)
end
---Visibility function: controls which columns are shown
---Values can be "show" or "hide"
---"buddy" makes a column invisible but keeps it for sorting purposes
---@param visibility table Array of visibility settings for each column
local Visibility = function(visibility)
tblVisibility = shallow_copy(visibility)
end
---Sort function: defines the sort order for each column
---Lower numbers = higher priority in sorting
---This determines the default sort order when results are displayed
---@param sort table Array of sort priorities for each column
local Sort = function(sort)
tblSort = shallow_copy(sort)
end
---Width function: defines the width for each column
---@param width table Array of widths for each column
local Width = function(width)
tblResultWidth = shallow_copy(width)
end
---Display function: outputs all collected results to Family Historian's result window
---This is the final step that shows all the collected data to the user
local Display = function()
if iRes > 0 then -- there are results to display
-- Set the window title
fhOutputResultSetTitles(strTitle)
-- Output each column with its configuration
-- This creates the actual result set in FH's display
for i, _ in ipairs(tblResults) do
fhOutputResultSetColumn(
tblResultHeadings[i], -- Column header
tblResultType[i], -- Data type
tblResults[i], -- The data for this column
iRes, -- Number of rows
tblResultWidth[i] or 80, -- Column width or 80 if not set
"align_left", -- Text alignment
tblSort[i], -- Sort priority
true, -- Sortable
"default", -- Sort direction
tblVisibility[i] -- Visibility setting
)
end
-- Update the display to show the results
fhUpdateDisplay()
else
-- No results found - show informational message
fhMessageBox(strNoResults, "MB_OK", "MB_ICONINFORMATION")
end
end
--expose public methods - return an object with all the public functions
-- This creates the public interface for the Results class
return {
Title = Title,
Headings = Headings,
Visibility = Visibility,
Types = Types,
Update = Update,
Display = Display,
Sort = Sort,
NoResults = NoResults,
Width = Width,
}
end
---getAllRecords function: Retrieves all records in the FH database except the focus record
---This creates a comprehensive list of all records to check for links
---@return table Array of ItemPointer objects representing all records
local function getAllRecords()
local recs = {}
---allrecords function: Creates an iterator for all records of a specific type
---This is a helper function that efficiently iterates through records
---@param type string The record type to iterate through
---@return function Iterator function that returns ItemPointer objects
local function allrecords(type)
local pi = fhNewItemPtr() -- Primary pointer for iteration
local p2 = fhNewItemPtr() -- Secondary pointer for returning current record
pi:MoveToFirstRecord(type) -- Start at the first record of this type
return function()
p2:MoveTo(pi) -- Copy current position to return pointer
pi:MoveNext() -- Move to next record
if p2:IsNotNull() then
return p2 -- Return the current record
end
end
end
-- Get the total number of record types in the database
local recordTypeCount = fhGetRecordTypeCount()
-- Iterate through all record types
for i = 0, recordTypeCount - 1 do
local recordType = fhGetRecordTypeTag(i) -- Get the tag for this record type
-- Iterate through all records of this type
for ptr in allrecords(recordType) do
-- Only include records that are not the same as our focus record
-- This prevents self-referencing and reduces unnecessary processing
if ptr:IsNotNull() and not ptr:IsSame(ptrFocus) then
table.insert(recs, ptr:Clone()) -- Add a copy to our list
end
end
end
return recs
end
-------------------------------------
--GET LINKED RECORDS
-------------------------------------
---GetLinkedRecords function: Gets all records and fields that are linked to the focus record, both Incoming and Outgoing, and updates myResults
---This is the core function that finds all links to and from the focus record
---@param ptrFocus ItemPointer The item to get linked records for
local function getLinkedRecords(ptrFocus)
-- Get all records in the database (except the focus record)
local tblCheck = getAllRecords()
---Recursive function to check all fields in a record for links
---@param direction string The direction of the link (From or To)
---@param record ItemPointer The record to check
---@param target ItemPointer The target record to find links to
---@param source ItemPointer The source record (for results display)
local function checkFieldsForLinks(direction, record, target, source)
local ptrField = fhNewItemPtr()
ptrField:MoveToFirstChildItem(record) -- Start with the first field
-- Iterate through all fields in this record
while ptrField:IsNotNull() do
-- Check if this field contains a link to our target record
local linkValue = fhGetValueAsLink(ptrField)
if linkValue and linkValue:IsNotNull() and linkValue:IsSame(target) then
-- Found a link! Add it to our results
local ptrParent = fhNewItemPtr()
ptrParent:MoveToParentItem(ptrField)
local strField = fhGetDisplayText(ptrField)
local ptrRole = fhGetItemPtr(ptrField, "~.ROLE") --only present for _SHAR or ASSO tags
if ptrRole:IsNotNull() then
strField = strField .. " (" .. fhGetDisplayText(ptrRole) .. ")"
end --special handling for witnesses
local ptrDate = fhGetItemPtr(ptrParent, "~.DATE")
local dpDate = fhNewDatePt()
if ptrDate:IsNotNull() then
local dtDate = fhGetValueAsDate(ptrDate)
dpDate = dtDate:GetDatePt1()
end
myResults.Update({
direction, -- "From" or "To" indicating link direction
source:Clone(), -- The record containing this field
fhGetRecordId(source), -- The record ID of the source
fhGetDisplayText(ptrParent), -- The parent field description
ptrParent:Clone(), --The parent field itself
dpDate:Clone(), --The date of the item
strField, -- The field name/label
ptrField:Clone(), -- The field itself
target:Clone(), -- The target record being linked to
fhGetRecordId(target), -- The record ID of the target
})
end
-- Recursively check child fields
checkFieldsForLinks(direction, ptrField, target, source)
ptrField:MoveNext() -- Move to next field
end
end
--Check each record for links to/from the focus record
for _, record in ipairs(tblCheck) do
--Check Outgoing Links (records that ptrFocus links to)
-- This finds records that the focus record points to
if fhCallBuiltInFunction("LinksFrom", ptrFocus, record, "EXT") > 0 then
-- Find the specific fields in ptrFocus that link to this record
checkFieldsForLinks("From", ptrFocus, record, ptrFocus)
end
--Check Incoming Links (records that link to ptrFocus)
-- This finds records that point to the focus record
if fhCallBuiltInFunction("LinksFrom", record, ptrFocus, "EXT") > 0 then
-- Find the specific fields in this record that link to ptrFocus
checkFieldsForLinks("To", record, ptrFocus, record)
end
end
end
-------------------------------------
--MAIN
-------------------------------------
-- Get the currently selected record or the Property Box record, to be analysed for links
local ptrRecs = fhGetCurrentRecordSel()
if #ptrRecs == 0 or ptrRecs[1]:IsNull() then
-- No record selected - check for a property box record
ptrFocus = fhGetCurrentPropertyBoxRecord()
if ptrFocus:IsNull() then
-- No record available to analyze
fhMessageBox("No selected record or property box record found", "MB_OK", "MB_ICONSTOP")
return
end
elseif #ptrRecs > 1 then
fhMessageBox("Multiple records selected - please select only one", "MB_OK", "MB_ICONSTOP")
return
else
-- Use the first selected record
ptrFocus = ptrRecs[1]
end
-- Configure the result set for displaying our findings
myResults = Results(10) -- Initialize results handling with 9 columns
myResults.Title("Link Lister: " .. fhGetDisplayText(ptrFocus)) -- Set window title with record name
myResults.NoResults("No links found") -- Message when no links are discovered
myResults.Headings({ "Direction", "From", "From ID", "Context", "", "Date", "Field", "", "To", "To ID" }) -- Column headers
myResults.Types({ "text", "item", "integer", "text", "item", "date-point", "text", "item", "item", "integer" }) -- Data types for each column
myResults.Visibility({ "hide", "show", "show", "show", "buddy", "show", "show", "buddy", "show", "show" }) -- Which columns to show
myResults.Width({ 0, 180, 25, 180, 0, 80, 180, 0, 180, 25 }) -- Width for each column
myResults.Sort({ 1, 2, 3, 7, 0, 8, 6, 0, 9, 10 }) -- Sort by Direction, then From Record and Field, then Context and Date, and finally To Record
-- Process the selected record to find incoming and outgoing links
getLinkedRecords(ptrFocus)
-- Display all the results we found
myResults.Display()
Source:Link-Lister-1.fh_lua